├── .nvmrc ├── spotlight-mock-server ├── .gitignore ├── package.json └── server.js ├── src ├── components │ ├── Dashboard │ │ ├── Dashboard.css │ │ ├── components │ │ │ ├── CopyEmbedCode.css │ │ │ ├── DashboardHeader.css │ │ │ ├── DashboardEvents.js │ │ │ ├── ToggleEvents.js │ │ │ ├── EventList.js │ │ │ ├── FilterEvents.js │ │ │ ├── DashboardEvents.css │ │ │ ├── SortEvents.js │ │ │ ├── EventList.css │ │ │ ├── DashboardHeader.js │ │ │ ├── EventListHelpers.js │ │ │ ├── CopyEmbedCode.js │ │ │ └── EventActions.js │ │ └── Dashboard.js │ ├── Users │ │ ├── components │ │ │ ├── UserActions.css │ │ │ ├── AddUser.css │ │ │ ├── AddUser.js │ │ │ ├── UserList.css │ │ │ ├── EditUser.css │ │ │ ├── UserActions.js │ │ │ └── UserList.js │ │ ├── Users.css │ │ └── Users.js │ ├── Common │ │ ├── images │ │ │ └── reconnection-icon.png │ │ ├── NoEvents.js │ │ ├── Loading.js │ │ ├── Loading.css │ │ ├── VideoHolder.js │ │ ├── CopyToClipboard.js │ │ ├── DatePicker.js │ │ ├── NetworkReconnect.js │ │ ├── NetworkReconnect.css │ │ ├── Chat.css │ │ └── Chat.js │ ├── Broadcast │ │ ├── Fan │ │ │ ├── components │ │ │ │ ├── FanHLSPlayer.css │ │ │ │ ├── FanStatusBar.css │ │ │ │ ├── FanStatusBar.js │ │ │ │ ├── FanHLSPlayer.js │ │ │ │ ├── FanHeader.css │ │ │ │ ├── FanBody.css │ │ │ │ ├── FanHeader.js │ │ │ │ └── FanBody.js │ │ │ └── Fan.css │ │ ├── Producer │ │ │ ├── Producer.css │ │ │ ├── components │ │ │ │ ├── ControlIcon.js │ │ │ │ ├── ProducerSidePanel.js │ │ │ │ ├── ProducerSidePanel.css │ │ │ │ ├── ProducerPrimary.css │ │ │ │ ├── ProducerChat.css │ │ │ │ ├── ActiveFanList.css │ │ │ │ ├── ProducerHeader.css │ │ │ │ ├── ProducerPrimary.js │ │ │ │ ├── Participant.css │ │ │ │ ├── ProducerHeader.js │ │ │ │ └── Participant.js │ │ │ └── Producer.js │ │ └── CelebrityHost │ │ │ ├── components │ │ │ ├── CelebrityHostBody.css │ │ │ ├── CelebrityHostHeader.css │ │ │ ├── CelebrityHostBody.js │ │ │ └── CelebrityHostHeader.js │ │ │ ├── CelebrityHost.css │ │ │ └── CelebrityHost.js │ ├── App │ │ ├── App.js │ │ └── App.css │ ├── Logout │ │ ├── Logout.css │ │ └── Logout.js │ ├── UpdateEvent │ │ ├── UpdateEvent.css │ │ ├── components │ │ │ └── EventForm.css │ │ └── UpdateEvent.js │ ├── Header │ │ ├── Header.css │ │ └── Header.js │ ├── ViewEvent │ │ ├── ViewEvent.css │ │ └── ViewEvent.js │ ├── AuthRoutes │ │ └── AuthRoutes.js │ ├── Login │ │ ├── components │ │ │ ├── LoginForm.css │ │ │ └── LoginForm.js │ │ ├── Login.css │ │ └── Login.js │ └── Alert │ │ └── Alert.js ├── images │ ├── 404.png │ ├── 500.png │ ├── logo.png │ ├── loading.gif │ ├── tb-icon.png │ ├── alerticons │ │ ├── basic.png │ │ ├── autotime.png │ │ ├── picture.png │ │ ├── success.png │ │ ├── textunder.png │ │ ├── warning.png │ │ └── anotherwarning.png │ ├── modalicons │ │ ├── large.png │ │ ├── normal.png │ │ └── small.png │ ├── opentok-meet-logo.png │ ├── TAB_VIDEO_PREVIEW_LS.jpg │ └── datatables │ │ ├── sort_asc.png │ │ ├── sort_both.png │ │ ├── sort_desc.png │ │ ├── back_disabled.png │ │ ├── back_enabled.png │ │ ├── forward_disabled.png │ │ ├── forward_enabled.png │ │ ├── back_enabled_hover.png │ │ ├── sort_asc_disabled.png │ │ ├── sort_desc_disabled.png │ │ └── forward_enabled_hover.png ├── config │ ├── api.js.example │ └── firebase.js.example ├── services │ ├── firebase.js │ ├── createEmbed.js │ ├── snapshot.js │ ├── localStorage.js │ ├── util.js │ ├── eventUrls.js │ ├── eventFiltering.js │ ├── logging.js │ └── api.js ├── reducers │ ├── currentUser.js │ ├── users.js │ ├── alert.js │ ├── root.js │ ├── auth.js │ ├── fan.js │ └── events.js ├── index.js ├── actions │ ├── currentUser.js │ ├── auth.js │ ├── alert.js │ └── users.js ├── configureStore.js ├── routes.js └── index.css ├── public ├── favicon.ico ├── embed-test.html ├── index.html └── embed.js ├── .flowconfig ├── static.json ├── CHANGELOG.md ├── .jsbeautifyrc ├── .gitignore ├── .github └── workflows │ └── metrics.yml ├── flowtypes ├── auth.js ├── alert.js ├── user.js ├── events.js └── types.js ├── LICENSE ├── .travis.yml ├── package.json ├── .eslintrc.json ├── CONTRIBUTING.md └── CODE_OF_CONDUCT.md /.nvmrc: -------------------------------------------------------------------------------- 1 | stable -------------------------------------------------------------------------------- /spotlight-mock-server/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | node_modules 3 | 4 | -------------------------------------------------------------------------------- /src/components/Dashboard/Dashboard.css: -------------------------------------------------------------------------------- 1 | .Dashboard { 2 | 3 | } -------------------------------------------------------------------------------- /src/components/Users/components/UserActions.css: -------------------------------------------------------------------------------- 1 | .UserActions { 2 | display: flex; 3 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/interactive-broadcast-js/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/interactive-broadcast-js/HEAD/src/images/404.png -------------------------------------------------------------------------------- /src/images/500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/interactive-broadcast-js/HEAD/src/images/500.png -------------------------------------------------------------------------------- /src/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/interactive-broadcast-js/HEAD/src/images/logo.png -------------------------------------------------------------------------------- /src/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/interactive-broadcast-js/HEAD/src/images/loading.gif -------------------------------------------------------------------------------- /src/images/tb-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/interactive-broadcast-js/HEAD/src/images/tb-icon.png -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | flowtypes/ 8 | 9 | [options] 10 | 11 | -------------------------------------------------------------------------------- /src/images/alerticons/basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/interactive-broadcast-js/HEAD/src/images/alerticons/basic.png -------------------------------------------------------------------------------- /src/images/modalicons/large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/interactive-broadcast-js/HEAD/src/images/modalicons/large.png -------------------------------------------------------------------------------- /src/images/modalicons/normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/interactive-broadcast-js/HEAD/src/images/modalicons/normal.png -------------------------------------------------------------------------------- /src/images/modalicons/small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/interactive-broadcast-js/HEAD/src/images/modalicons/small.png -------------------------------------------------------------------------------- /src/images/opentok-meet-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/interactive-broadcast-js/HEAD/src/images/opentok-meet-logo.png -------------------------------------------------------------------------------- /src/images/TAB_VIDEO_PREVIEW_LS.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/interactive-broadcast-js/HEAD/src/images/TAB_VIDEO_PREVIEW_LS.jpg -------------------------------------------------------------------------------- /src/images/alerticons/autotime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/interactive-broadcast-js/HEAD/src/images/alerticons/autotime.png -------------------------------------------------------------------------------- /src/images/alerticons/picture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/interactive-broadcast-js/HEAD/src/images/alerticons/picture.png -------------------------------------------------------------------------------- /src/images/alerticons/success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/interactive-broadcast-js/HEAD/src/images/alerticons/success.png -------------------------------------------------------------------------------- /src/images/alerticons/textunder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/interactive-broadcast-js/HEAD/src/images/alerticons/textunder.png -------------------------------------------------------------------------------- /src/images/alerticons/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/interactive-broadcast-js/HEAD/src/images/alerticons/warning.png -------------------------------------------------------------------------------- /src/images/datatables/sort_asc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/interactive-broadcast-js/HEAD/src/images/datatables/sort_asc.png -------------------------------------------------------------------------------- /src/images/datatables/sort_both.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/interactive-broadcast-js/HEAD/src/images/datatables/sort_both.png -------------------------------------------------------------------------------- /src/images/datatables/sort_desc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/interactive-broadcast-js/HEAD/src/images/datatables/sort_desc.png -------------------------------------------------------------------------------- /src/images/alerticons/anotherwarning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/interactive-broadcast-js/HEAD/src/images/alerticons/anotherwarning.png -------------------------------------------------------------------------------- /src/images/datatables/back_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/interactive-broadcast-js/HEAD/src/images/datatables/back_disabled.png -------------------------------------------------------------------------------- /src/images/datatables/back_enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/interactive-broadcast-js/HEAD/src/images/datatables/back_enabled.png -------------------------------------------------------------------------------- /src/images/datatables/forward_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/interactive-broadcast-js/HEAD/src/images/datatables/forward_disabled.png -------------------------------------------------------------------------------- /src/images/datatables/forward_enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/interactive-broadcast-js/HEAD/src/images/datatables/forward_enabled.png -------------------------------------------------------------------------------- /static.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "build/", 3 | "clean_urls": false, 4 | "routes": { 5 | "/**": "index.html" 6 | }, 7 | "https_only": true 8 | } -------------------------------------------------------------------------------- /src/images/datatables/back_enabled_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/interactive-broadcast-js/HEAD/src/images/datatables/back_enabled_hover.png -------------------------------------------------------------------------------- /src/images/datatables/sort_asc_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/interactive-broadcast-js/HEAD/src/images/datatables/sort_asc_disabled.png -------------------------------------------------------------------------------- /src/images/datatables/sort_desc_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/interactive-broadcast-js/HEAD/src/images/datatables/sort_desc_disabled.png -------------------------------------------------------------------------------- /src/images/datatables/forward_enabled_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/interactive-broadcast-js/HEAD/src/images/datatables/forward_enabled_hover.png -------------------------------------------------------------------------------- /src/components/Common/images/reconnection-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opentok/interactive-broadcast-js/HEAD/src/components/Common/images/reconnection-icon.png -------------------------------------------------------------------------------- /src/components/Broadcast/Fan/components/FanHLSPlayer.css: -------------------------------------------------------------------------------- 1 | .FanHLSPlayer { 2 | width: 100%; 3 | } 4 | 5 | #hlsjslive { 6 | background-color: #000000; 7 | max-height: 500px; 8 | } 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # OpenTok Interactive Broadcasting 2 | All notable changes to this project will be documented in this file. 3 | 4 | -------------------------------------- 5 | ####[2.0.0] 6 | 7 | * [ADDED] Official release -------------------------------------------------------------------------------- /src/config/api.js.example: -------------------------------------------------------------------------------- 1 | const api = { 2 | localhost: 'http://localhost:3001', // Replace this url for your local API server 3 | production: '', // Replace this url for your production API server 4 | }; 5 | 6 | export default api; 7 | -------------------------------------------------------------------------------- /src/components/Common/NoEvents.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | 4 | const NoEvents = (): ReactComponent => 5 |
6 | There are no upcoming events. 7 |
; 8 | 9 | export default NoEvents; 10 | -------------------------------------------------------------------------------- /.jsbeautifyrc: -------------------------------------------------------------------------------- 1 | { 2 | "js":{ 3 | "indent_size": 2, 4 | "space_after_anon_function": true, 5 | "end_with_newline": true, 6 | "brace_style": "collapse-preserve-inline", 7 | "e4x": true 8 | } 9 | } 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/services/firebase.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import firebase from 'firebase/app'; 3 | import 'firebase/auth'; 4 | import 'firebase/database'; 5 | import 'firebase/storage'; 6 | import firebaseConfig from '../config/firebase'; 7 | 8 | firebase.initializeApp(firebaseConfig); 9 | 10 | export default firebase; 11 | 12 | -------------------------------------------------------------------------------- /src/reducers/currentUser.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const user = (state: CurrentUserState = null, action: UserAction): CurrentUserState => { 4 | switch (action.type) { 5 | case 'SET_CURRENT_USER': 6 | return action.user; 7 | default: 8 | return state; 9 | } 10 | }; 11 | 12 | export default user; 13 | -------------------------------------------------------------------------------- /src/components/Common/Loading.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import './Loading.css'; 4 | import loadingImage from '../../images/loading.gif'; 5 | 6 | const Loading = (): ReactComponent => 7 |
8 | loading-img 9 |
; 10 | 11 | export default Loading; 12 | -------------------------------------------------------------------------------- /src/components/Broadcast/Producer/Producer.css: -------------------------------------------------------------------------------- 1 | .Producer { 2 | display: flex; 3 | position: relative; 4 | width: 100vw; 5 | } 6 | .Producer-main { 7 | height: 100vh; 8 | width: calc(100% - 320px); 9 | min-width: 750px; 10 | transition: width .5s; 11 | } 12 | 13 | .Producer-main.full { 14 | width: 100%; 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/config/firebase.js.example: -------------------------------------------------------------------------------- 1 | const firebase = { 2 | apiKey: '', // firebase apikey 3 | authDomain: '', // firebase authDomain 4 | databaseURL: '', // firebase databaseURL 5 | projectId: '', // firebase projectId 6 | storageBucket: '', // firebase storageBucket 7 | messagingSenderId: '', // firebase messagingSenderId 8 | }; 9 | 10 | export default firebase; 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # config 4 | /src/config/firebase.js 5 | /src/config/api.js 6 | 7 | # dependencies 8 | /node_modules 9 | 10 | # testing 11 | /coverage 12 | 13 | # production 14 | /build 15 | 16 | # misc 17 | .DS_Store 18 | .env 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | yarn.lock* 23 | -------------------------------------------------------------------------------- /src/components/App/App.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import Header from '../Header/Header'; 4 | import Alert from '../Alert/Alert'; 5 | import './App.css'; 6 | 7 | const App = ({ children }: { children: ReactComponent }): ReactComponent => 8 | (
9 | 10 |
11 | { children } 12 |
); 13 | 14 | export default App; 15 | -------------------------------------------------------------------------------- /spotlight-mock-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spotlight-mock-server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "body-parser": "^1.17.1", 13 | "cors": "^2.8.1", 14 | "express": "^4.15.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/components/Common/Loading.css: -------------------------------------------------------------------------------- 1 | /* Page loading */ 2 | .Loading { 3 | position: fixed; 4 | left: 0px; 5 | top: 0px; 6 | width: 100%; 7 | height: 100%; 8 | z-index: 99999; 9 | background: #3D464D; 10 | opacity: 0.99; 11 | } 12 | .Loading img { 13 | width: 40px; 14 | height: 40px; 15 | position: absolute; 16 | left: 50%; 17 | right: 50%; 18 | bottom: 50%; 19 | top: 50%; 20 | margin: -20px; 21 | } 22 | -------------------------------------------------------------------------------- /src/components/Common/VideoHolder.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import classNames from 'classnames'; 4 | 5 | type Props = { userType: ParticipantType, connected: boolean, isMe?: boolean }; 6 | const VideoHolder = ({ userType, connected, isMe }: Props): ReactComponent => 7 |
; 8 | 9 | export default VideoHolder; 10 | -------------------------------------------------------------------------------- /src/components/Logout/Logout.css: -------------------------------------------------------------------------------- 1 | .Logout { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | font-size: 14px; 6 | color: white; 7 | font-weight: 300; 8 | margin-right: 5px; 9 | } 10 | 11 | .Logout .divider { 12 | position: relative; 13 | top: -2px; 14 | margin-left: 5px; 15 | font-size: 18px; 16 | } 17 | 18 | .Logout .btn .fa { 19 | margin-top: 2px; 20 | margin-left: 5px; 21 | } -------------------------------------------------------------------------------- /src/components/Users/components/AddUser.css: -------------------------------------------------------------------------------- 1 | .AddUser { 2 | position: relative; 3 | justify-content: flex-start !important; 4 | } 5 | 6 | .AddUser .header-container { 7 | position: absolute; 8 | top: 0; 9 | height: 100%; 10 | right: 10%; 11 | color: #607d8b; 12 | display: flex; 13 | flex-direction: column; 14 | justify-content: center; 15 | } 16 | 17 | .AddUser .header-container h4 { 18 | margin: 10px 0 0; 19 | } -------------------------------------------------------------------------------- /src/components/App/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 80px; 8 | } 9 | 10 | .App-header { 11 | background-color: #222; 12 | height: 150px; 13 | padding: 20px; 14 | color: white; 15 | } 16 | 17 | .App-intro { 18 | font-size: large; 19 | } 20 | 21 | @keyframes App-logo-spin { 22 | from { transform: rotate(0deg); } 23 | to { transform: rotate(360deg); } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/UpdateEvent/UpdateEvent.css: -------------------------------------------------------------------------------- 1 | .UpdateEvent-header { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: flex-start; 5 | justify-content: center; 6 | padding: 10px 20px; 7 | } 8 | 9 | .UpdateEvent-header a { 10 | color: #00a3e3; 11 | font-size: 13px; 12 | } 13 | 14 | .UpdateEvent-header h3 { 15 | color: #37363E; 16 | font-family: 'Montserrat', sans-serif; 17 | font-size: 22px; 18 | font-weight: 300; 19 | margin: 5px 0; 20 | } -------------------------------------------------------------------------------- /src/components/Broadcast/Fan/components/FanStatusBar.css: -------------------------------------------------------------------------------- 1 | .FanStatusBar { 2 | width: auto; 3 | font-size: 16px; 4 | padding: 0; 5 | color: #fff; 6 | line-height: 30px; 7 | font-weight: 400; 8 | text-align: center; 9 | } 10 | 11 | .FanStatusBar.lightBlue { 12 | background: #8EB9C3; 13 | } 14 | 15 | .FanStatusBar.blue { 16 | background: #00a3e3; 17 | } 18 | 19 | .FanStatusBar.green { 20 | background: #26A65B; 21 | } 22 | 23 | .FanStatusBar.red { 24 | background: #EF4836; 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/metrics.yml: -------------------------------------------------------------------------------- 1 | name: Aggregit 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | jobs: 8 | recordMetrics: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: michaeljolley/aggregit@v1 12 | with: 13 | githubToken: ${{ secrets.GITHUB_TOKEN }} 14 | project_id: ${{ secrets.project_id }} 15 | private_key: ${{ secrets.private_key }} 16 | client_email: ${{ secrets.client_email }} 17 | firebaseDbUrl: ${{ secrets.firebaseDbUrl }} 18 | -------------------------------------------------------------------------------- /src/components/Header/Header.css: -------------------------------------------------------------------------------- 1 | .Header { 2 | height: 60px; 3 | display: flex; 4 | justify-content: space-between; 5 | color: white; 6 | background-color: #00a3e3; 7 | } 8 | 9 | .Header-logo { 10 | width: 250px; 11 | height: 100%; 12 | padding: 0 20px; 13 | display: flex; 14 | justify-content: center; 15 | align-items: center; 16 | font-size: 12px; 17 | font-weight: 300; 18 | font-family: 'Montserrat', sans-serif; 19 | background-color: rgba(0, 0, 0, 0.05); 20 | } -------------------------------------------------------------------------------- /src/reducers/users.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import R from 'ramda'; 3 | 4 | const users = (state: UserMap = {}, action: ManageUsersAction): UserMap => { 5 | switch (action.type) { 6 | case 'SET_USERS': 7 | return action.users; 8 | case 'UPDATE_USER': 9 | return R.assoc(action.user.id, R.merge(state[action.user.id], action.user), state); 10 | case 'REMOVE_USER': 11 | return R.omit([action.userId], state); 12 | default: 13 | return state; 14 | } 15 | }; 16 | 17 | export default users; 18 | -------------------------------------------------------------------------------- /src/components/Broadcast/Producer/components/ControlIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon from 'react-fontawesome'; 3 | 4 | type Props = { 5 | className: Classes, 6 | name: string, 7 | onClick: Handler, 8 | disabled: boolean 9 | }; 10 | 11 | const ControlIcon = ({ className, name, onClick, disabled = false }: Props): ReactComponent => 12 | ; 15 | 16 | export default ControlIcon; 17 | -------------------------------------------------------------------------------- /src/components/Users/components/AddUser.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import Icon from 'react-fontawesome'; 4 | import EditUser from './EditUser'; 5 | import './AddUser.css'; 6 | 7 | 8 | const AddUser = (): ReactComponent => 9 |
10 |
11 | 12 |

Create New User

13 |
14 | 15 |
; 16 | 17 | export default AddUser; 18 | -------------------------------------------------------------------------------- /src/components/ViewEvent/ViewEvent.css: -------------------------------------------------------------------------------- 1 | .ViewEvent .ViewEvent-header { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: flex-start; 5 | justify-content: center; 6 | padding: 10px 20px; 7 | background: #FFFFFF; 8 | } 9 | 10 | .ViewEvent .ViewEvent-header a { 11 | color: #00a3e3; 12 | font-size: 13px; 13 | } 14 | 15 | .ViewEvent .ViewEvent-header h3 { 16 | color: #37363E; 17 | font-family: 'Montserrat', sans-serif; 18 | font-size: 22px; 19 | font-weight: 300; 20 | margin: 5px 0; 21 | } 22 | -------------------------------------------------------------------------------- /src/components/Users/Users.css: -------------------------------------------------------------------------------- 1 | .Users {} 2 | 3 | .UsersHeader { 4 | display: flex; 5 | flex-direction: column; 6 | align-items: flex-start; 7 | justify-content: center; 8 | padding: 10px 20px; 9 | } 10 | 11 | .UsersHeader a { 12 | color: #00a3e3; 13 | font-size: 13px; 14 | } 15 | 16 | .UsersHeader h3 { 17 | color: #37363E; 18 | font-family: 'Montserrat', sans-serif; 19 | font-size: 22px; 20 | font-weight: 300; 21 | margin: 5px 0; 22 | } 23 | 24 | .Users-list-container { 25 | background: #F5F5F5; 26 | padding: 20px; 27 | } -------------------------------------------------------------------------------- /flowtypes/auth.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* eslint no-undef: "off" */ 3 | /* beautify preserve:start */ 4 | 5 | declare type AuthToken = string; 6 | 7 | declare type AuthState = { 8 | error: boolean, 9 | forgotPassword: boolean, 10 | authToken: null | AuthToken 11 | }; 12 | 13 | declare type AuthCredentials = { email: string, password?: string }; 14 | declare type AuthAction = 15 | { type: 'AUTHENTICATE_USER', credentials: AuthCredentials } | 16 | { type: 'AUTH_ERROR', error: null | Error } | 17 | { type: 'AUTH_FORGOT_PASSWORD', forgot: boolean } | 18 | { type: 'SET_AUTH_TOKEN', token: string }; 19 | -------------------------------------------------------------------------------- /src/reducers/alert.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import R from 'ramda'; 3 | 4 | const initialState: AlertState = { 5 | show: false, 6 | type: 'info', 7 | title: '', 8 | text: '', 9 | onConfirm: null, 10 | showCancelButton: false, 11 | }; 12 | 13 | const alert = (state: AlertState = initialState, action: AlertAction): AlertState => { 14 | switch (action.type) { 15 | case 'RESET_ALERT': 16 | return R.merge(state, initialState); 17 | case 'SET_ALERT': 18 | return R.merge(state, action.options); 19 | default: 20 | return state; 21 | } 22 | }; 23 | 24 | export default alert; 25 | -------------------------------------------------------------------------------- /src/reducers/root.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { combineReducers } from 'redux'; 3 | import { reducer as toastr } from 'react-redux-toastr'; 4 | 5 | /** Reducers */ 6 | import auth from './auth'; 7 | import currentUser from './currentUser'; 8 | import users from './users'; 9 | import events from './events'; 10 | import broadcast from './broadcast'; 11 | import alert from './alert'; 12 | import fan from './fan'; 13 | 14 | /** Combine Reducers */ 15 | const interactiveBroadcastApp = combineReducers({ auth, currentUser, users, events, broadcast, alert, toastr, fan }); 16 | 17 | export default interactiveBroadcastApp; 18 | -------------------------------------------------------------------------------- /public/embed-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | IBS embed test 7 | 8 | 9 |
10 | 11 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/reducers/auth.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import R from 'ramda'; 3 | 4 | const initialState = { error: false, forgotPassword: false, authToken: null }; 5 | const auth = (state: AuthState = initialState, action: AuthAction): AuthState => { 6 | switch (action.type) { 7 | case 'AUTH_ERROR': 8 | return R.assoc('error', action.error, state); 9 | case 'AUTH_FORGOT_PASSWORD': 10 | return R.assoc('forgotPassword', action.forgot, state); 11 | case 'SET_AUTH_TOKEN': 12 | return R.assoc('authToken', action.token, state); 13 | default: 14 | return state; 15 | } 16 | }; 17 | 18 | export default auth; 19 | -------------------------------------------------------------------------------- /src/services/createEmbed.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const origin = window.location.origin; 3 | 4 | const createEmbed = (userType: UserRole, adminId: string): string => { 5 | if (!adminId) { return ''; } 6 | return `
7 | 8 | `; 18 | }; 19 | 20 | export default createEmbed; 21 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { Provider } from 'react-redux'; 4 | import { render } from 'react-dom'; 5 | import ReduxToastr from 'react-redux-toastr'; 6 | import 'babel-polyfill'; 7 | import routes from './routes'; 8 | import configureStore from './configureStore'; 9 | import './index.css'; 10 | 11 | render(( 12 | 13 |
14 | { routes } 15 | 19 |
20 |
21 | ), document.getElementById('root')); 22 | 23 | -------------------------------------------------------------------------------- /src/components/Dashboard/components/CopyEmbedCode.css: -------------------------------------------------------------------------------- 1 | .CopyEmbedCode { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | .CopyEmbedCode .btn.toggle .fa { 8 | margin-left: 5px; 9 | } 10 | 11 | .CopyEmbedCode-button-container { 12 | width: 100%; 13 | position: absolute; 14 | visibility: hidden; 15 | display: flex; 16 | flex-direction: column; 17 | top: 110%; 18 | border: 1px solid lightgrey; 19 | } 20 | 21 | .CopyEmbedCode-button-container .btn.white { 22 | border: none; 23 | width: 100%; 24 | margin: 0; 25 | } 26 | 27 | .CopyEmbedCode-button-container.expanded { 28 | visibility: visible; 29 | } 30 | 31 | -------------------------------------------------------------------------------- /src/components/Broadcast/Producer/components/ProducerSidePanel.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import classNames from 'classnames'; 4 | import ActiveFanList from './ActiveFanList'; 5 | import './ProducerSidePanel.css'; 6 | 7 | type Props = { hidden: boolean, broadcast: BroadcastState }; 8 | 9 | const ProducerSidePanel = ({ hidden, broadcast }: Props): ReactComponent => 10 | ; 14 | 15 | export default ProducerSidePanel; 16 | -------------------------------------------------------------------------------- /src/components/Common/CopyToClipboard.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import Copy from 'react-copy-to-clipboard'; 4 | import R from 'ramda'; 5 | import { toastr } from 'react-redux-toastr'; 6 | 7 | type Props = { 8 | children?: ReactComponent[], 9 | text: string, 10 | onCopyText: string 11 | }; 12 | 13 | const onCopy = (text: string): void => toastr.success('Success', `${text} copied to clipboard`, { icon: 'success' }); 14 | 15 | const CopyToClipboard = ({ children, text = '', onCopyText }: Props): ReactComponent => 16 | 17 | { children } 18 | ; 19 | 20 | export default CopyToClipboard; 21 | -------------------------------------------------------------------------------- /src/components/Users/components/UserList.css: -------------------------------------------------------------------------------- 1 | .UserList-item { 2 | /*position: relative;*/ 3 | } 4 | 5 | .UserList-item .user-info { 6 | display: flex; 7 | align-items: center; 8 | color: #58666e; 9 | font-size: 14px; 10 | } 11 | 12 | .UserList-item .user-info span { 13 | line-height: 20px; 14 | } 15 | 16 | .UserList-item .user-info .name { 17 | margin-right: 5px; 18 | font-family: 'Montserrat', sans-serif; 19 | font-weight: 600; 20 | } 21 | 22 | .UserList-item .user-info .email { 23 | font-weight: 300; 24 | } 25 | 26 | .EditUser-warning { 27 | text-align: left; 28 | color: #EF4836; 29 | } 30 | 31 | .EditUser-warning i { 32 | padding-right: 5px; 33 | } 34 | -------------------------------------------------------------------------------- /src/components/Header/Header.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { withRouter } from 'react-router'; 4 | import Logout from '../Logout/Logout'; 5 | import './Header.css'; 6 | 7 | type Props = { 8 | routes: Route[] 9 | }; 10 | 11 | const DefaultLogo = (): ReactComponent =>
Interactive Broadcasting Solution
; 12 | 13 | const Header = ({ routes }: Props): ReactComponent => { 14 | const currentRoute = routes[routes.length - 1]; 15 | if (currentRoute.hideHeader) return null; 16 | return ( 17 |
18 | 19 | 20 |
21 | ); 22 | }; 23 | 24 | 25 | export default withRouter(Header); 26 | -------------------------------------------------------------------------------- /src/components/Broadcast/Producer/components/ProducerSidePanel.css: -------------------------------------------------------------------------------- 1 | .ProducerSidePanel { 2 | position: absolute; 3 | height: 100%; 4 | background: white; 5 | right: 0; 6 | border-left: 1px solid rgba(0, 0, 0, 0.2); 7 | transition: right .5s; 8 | } 9 | 10 | .ProducerSidePanel.hidden { 11 | right: -320px; 12 | } 13 | 14 | .ProducerSidePanel-header { 15 | display: flex; 16 | justify-content: center; 17 | align-items: center; 18 | height: 55px; 19 | border-bottom: 1px solid rgba(0, 0, 0, 0.2); 20 | font-family: 'Montserrat', sans-serif; 21 | font-size: 14px; 22 | } 23 | 24 | .ProducerSidePanel-reordering { 25 | background: rgba(0, 0, 0, 0.1); 26 | opacity: .90; 27 | } 28 | -------------------------------------------------------------------------------- /src/components/Dashboard/components/DashboardHeader.css: -------------------------------------------------------------------------------- 1 | .DashboardHeader { 2 | display: flex; 3 | justify-content: space-between; 4 | padding: 10px 20px; 5 | } 6 | 7 | .DashboardHeader h3 { 8 | color: #37363E; 9 | font-family: 'Montserrat', sans-serif; 10 | font-size: 22px; 11 | font-weight: 300; 12 | } 13 | 14 | .DashboardHeader-controls { 15 | width: 650px; 16 | display: flex; 17 | align-items: center; 18 | justify-content: space-around; 19 | } 20 | 21 | .DashboardHeader-controls .btn { 22 | font-size: 12px; 23 | width: 150px; 24 | height: 34px; 25 | color: rgb(88, 102, 110); 26 | background-color: white; 27 | } 28 | 29 | .DashboardHeader-controls .btn .fa { 30 | margin: 1px 5px 0 0; 31 | } 32 | -------------------------------------------------------------------------------- /src/components/AuthRoutes/AuthRoutes.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { Component } from 'react'; 3 | import R from 'ramda'; 4 | import { connect } from 'react-redux'; 5 | import { browserHistory } from 'react-router'; 6 | 7 | type Props = { 8 | children: ReactComponent[], 9 | currentUser: User 10 | }; 11 | 12 | class AuthRoutes extends Component { 13 | props: Props; 14 | 15 | componentWillMount() { 16 | if (!this.props.currentUser) { 17 | browserHistory.replace('/'); 18 | } 19 | } 20 | 21 | render(): ReactComponent { 22 | return this.props.currentUser ? this.props.children : null; 23 | } 24 | } 25 | 26 | const mapStateToProps = (state: State): Props => R.pick(['currentUser'], state); 27 | export default connect(mapStateToProps)(AuthRoutes); 28 | 29 | -------------------------------------------------------------------------------- /src/services/snapshot.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const snapshot = async (imgData: string): Promise.resolve => 3 | new Promise((resolve: Promise.resolve, reject: Promise.reject) => { 4 | const tempImg = document.createElement('img'); 5 | tempImg.src = `data:image/png;base64,${imgData}`; 6 | tempImg.onload = () => { 7 | const canvas = document.createElement('canvas'); 8 | const ctx = canvas.getContext('2d'); 9 | canvas.width = 60; 10 | canvas.height = 60; 11 | if (ctx && tempImg) { 12 | ctx.drawImage(tempImg, 0, 0, canvas.width, canvas.height);// $FlowFixMe 13 | resolve(canvas.toDataURL()); 14 | } else { // $FlowFixMe 15 | reject(null); 16 | } 17 | }; 18 | }); 19 | 20 | export default snapshot; 21 | -------------------------------------------------------------------------------- /src/components/Dashboard/components/DashboardEvents.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import EventList from './EventList'; 4 | import FilterEvents from './FilterEvents'; 5 | import SortEvents from './SortEvents'; 6 | import { filterAndSort } from '../../../services/eventFiltering'; 7 | import './DashboardEvents.css'; 8 | 9 | /* beautify preserve:start */ 10 | type Props = { 11 | events: EventsState 12 | }; 13 | /* beautify preserve:end */ 14 | 15 | const DashboardEvents = ({ events }: Props): ReactComponent => 16 |
17 |
18 | 19 | 20 |
21 | 22 |
; 23 | 24 | export default DashboardEvents; 25 | 26 | -------------------------------------------------------------------------------- /src/components/Login/components/LoginForm.css: -------------------------------------------------------------------------------- 1 | .LoginForm { 2 | width: 100%; 3 | height: 220px; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: space-around; 7 | align-items: center; 8 | padding: 10px 0; 9 | } 10 | 11 | .LoginForm .input-container { 12 | position: relative; 13 | width: 78%; 14 | margin: 10px 0; 15 | } 16 | 17 | .LoginForm .input-container .icon { 18 | position: absolute; 19 | top: 12px; 20 | left: 14px; 21 | } 22 | 23 | .LoginForm .input-container input { 24 | height: 40px; 25 | width: 100%; 26 | font-size: 14px; 27 | } 28 | 29 | .LoginForm .input-container input[type=email], 30 | .LoginForm .input-container input[type=password] { 31 | border: 1px solid #BDC4C9; 32 | border-radius: 3px; 33 | box-shadow: inset 0px 1px 0px #F1F0F1; 34 | padding: 6px 12px 6px 40px; 35 | } -------------------------------------------------------------------------------- /src/actions/currentUser.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { browserHistory } from 'react-router'; 3 | import { getUser } from '../services/api'; 4 | 5 | const setCurrentUser: ActionCreator = (user: User): UserAction => ({ 6 | type: 'SET_CURRENT_USER', 7 | user, 8 | }); 9 | 10 | const logIn: ThunkActionCreator = (userId: string): Thunk => 11 | (dispatch: Dispatch) => { 12 | getUser(userId) 13 | .then((user: User) => { 14 | dispatch(setCurrentUser(user)); 15 | browserHistory.push('/admin'); 16 | }) 17 | .catch((error: Error): void => console.log(error)); // TODO Use alert to have user refresh 18 | }; 19 | 20 | const logOut: ThunkActionCreator = (): Thunk => 21 | (dispatch: Dispatch) => { 22 | dispatch(setCurrentUser(null)); 23 | }; 24 | 25 | module.exports = { 26 | logIn, 27 | logOut, 28 | setCurrentUser, 29 | }; 30 | -------------------------------------------------------------------------------- /src/components/Broadcast/Producer/components/ProducerPrimary.css: -------------------------------------------------------------------------------- 1 | .ProducerPrimary { 2 | padding: 30px; 3 | } 4 | 5 | .ProducerPrimary-info { 6 | display: flex; 7 | flex-direction: column; 8 | justify-content: flex-start; 9 | align-items: flex-start; 10 | color: #58666E; 11 | font-size: 12px; 12 | } 13 | 14 | .ProducerPrimary-info .private-call { 15 | width: 100%; 16 | background: #EF4836; 17 | color: white; 18 | opacity: 0; 19 | transition: opacity .75s ease-in-out; 20 | } 21 | 22 | .ProducerPrimary-info .private-call.active { 23 | opacity: 1; 24 | } 25 | 26 | .ProducerPrimary-participants { 27 | display: flex; 28 | flex-wrap: wrap; 29 | justify-content: space-between; 30 | align-items: center; 31 | padding: 15px 0; 32 | } 33 | 34 | .ProducerPrimary-participants .producerContainer { 35 | position: absolute; 36 | z-index: -100; 37 | top: -1px; 38 | } 39 | -------------------------------------------------------------------------------- /src/components/Login/Login.css: -------------------------------------------------------------------------------- 1 | .Login { 2 | width: 360px; 3 | display: flex; 4 | flex-direction: column; 5 | justify-content: center; 6 | align-items: center; 7 | margin: 60px auto 0; 8 | padding-bottom: 10px; 9 | border: 1px solid #ddd; 10 | border-radius: 3px; 11 | background-color: white; 12 | } 13 | 14 | .Login-header { 15 | width: 100%; 16 | padding: 15px 0; 17 | border-bottom: 1px solid #ddd; 18 | } 19 | 20 | .Login-header img { 21 | height: 75px; 22 | width: 75px; 23 | } 24 | 25 | .Login-messages { 26 | display: flex; 27 | flex-direction: column; 28 | justify-content: space-around; 29 | height: 50px; 30 | font-style: italic; 31 | } 32 | 33 | .Login-forgot { 34 | font-style: italic; 35 | } 36 | 37 | .Login-forgot:hover { 38 | background-color: initial !important; 39 | } 40 | 41 | .Login-error { 42 | color: red; 43 | font-weight: 600; 44 | } 45 | 46 | -------------------------------------------------------------------------------- /src/components/Dashboard/components/ToggleEvents.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import R from 'ramda'; 3 | import classNames from 'classnames'; 4 | 5 | type Props = { 6 | showing: string, 7 | toggle: 'all' | 'current' | 'archived' => void 8 | }; 9 | 10 | const ToggleEvents = ({ toggle, showing }: Props): ReactComponent => 11 |
12 | 15 | 18 | 21 |
; 22 | 23 | export default ToggleEvents; 24 | 25 | -------------------------------------------------------------------------------- /src/configureStore.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { createStore, applyMiddleware, compose } from 'redux'; 3 | import thunk from 'redux-thunk'; 4 | import throttle from 'lodash.throttle'; 5 | import { loadState, saveState } from './services/localStorage'; 6 | import interactiveBroadcastApp from './reducers/root'; 7 | 8 | // eslint-disable-next-line no-underscore-dangle 9 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 10 | const configureStore = (): Store => { 11 | const persistedState = loadState(); 12 | const store = createStore( 13 | interactiveBroadcastApp, 14 | persistedState, 15 | composeEnhancers(applyMiddleware(thunk)) // eslint-disable-line comma-dangle 16 | ); 17 | 18 | // What do we want to persist to local storage? 19 | store.subscribe(throttle(() => { 20 | saveState({ 21 | currentUser: store.getState().currentUser, 22 | }); 23 | }, 1000)); 24 | 25 | return store; 26 | }; 27 | 28 | export default configureStore; 29 | -------------------------------------------------------------------------------- /src/services/localStorage.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const storageKey = 'interactiveBroadcast'; 3 | 4 | export const loadState = (): LocalStorageState | void => { 5 | try { 6 | const serializedState = localStorage.getItem(storageKey); 7 | if (!serializedState) { 8 | return undefined; 9 | } 10 | return JSON.parse(serializedState); 11 | } catch (error) { 12 | return undefined; 13 | } 14 | }; 15 | 16 | export const saveState = (state: LocalStorageState) => { 17 | try { 18 | const serializedState = JSON.stringify(state); 19 | localStorage.setItem(storageKey, serializedState); 20 | } catch (error) { 21 | // Nothing to do, nowhere to go 22 | } 23 | }; 24 | 25 | export const saveAuthToken = (token: string): void => { 26 | try { 27 | localStorage.setItem(`${storageKey}-token`, token); 28 | } catch (error) { 29 | // Nothing to do, nowhere to go 30 | } 31 | }; 32 | 33 | export const loadAuthToken = (): string => localStorage.getItem(`${storageKey}-token`) || ''; 34 | 35 | -------------------------------------------------------------------------------- /src/components/Users/components/EditUser.css: -------------------------------------------------------------------------------- 1 | .EditUser { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | .EditUser-form .edit-user-top, 7 | .EditUser-form .edit-user-bottom { 8 | display: flex; 9 | align-items: center; 10 | margin: 10px 0; 11 | } 12 | 13 | .EditUser-form .input-container { 14 | position: relative; 15 | } 16 | 17 | .EditUser-form .edit-user-top input { 18 | font-size: 13px; 19 | height: 34px; 20 | border: 1px solid #BDC4C9; 21 | border-radius: 3px; 22 | box-shadow: inset 0px 1px 0px #F1F0F1; 23 | margin: 0 5px; 24 | padding: 6px 12px 6px 40px; 25 | } 26 | 27 | .EditUser-form .edit-user-bottom .input-container { 28 | margin-right: 10px; 29 | } 30 | 31 | .EditUser-form .edit-user-bottom .input-container .label { 32 | margin-left: 5px; 33 | font-size: 13px; 34 | font-weight: 700; 35 | color: #58666e; 36 | } 37 | 38 | .EditUser .input-container .icon { 39 | position: absolute; 40 | top: 10px; 41 | left: 14px; 42 | } -------------------------------------------------------------------------------- /src/components/Common/DatePicker.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import moment from 'moment'; 4 | import Datetime from 'react-datetime'; 5 | import 'react-datetime/css/react-datetime.css'; 6 | 7 | type Props = { 8 | name: string, 9 | value: string, 10 | onChange: Unit 11 | }; 12 | 13 | class DatePicker extends Component { 14 | props: Props; 15 | handleChange: moment => void; 16 | constructor(props: Props) { 17 | super(props); 18 | this.handleChange = this.handleChange.bind(this); 19 | } 20 | handleChange(m: moment) { 21 | const { onChange, name } = this.props; 22 | onChange({ target: { name, value: m.format('MM/DD/YYYY hh:mm:ss a') } }); 23 | } 24 | 25 | render(): ReactComponent { 26 | const { name, value } = this.props; 27 | const { handleChange } = this; 28 | return ; 29 | } 30 | } 31 | 32 | export default DatePicker; 33 | -------------------------------------------------------------------------------- /src/components/Broadcast/CelebrityHost/components/CelebrityHostBody.css: -------------------------------------------------------------------------------- 1 | .CelebrityHostBody { 2 | background-color: #000000; 3 | display: flex; 4 | justify-content: space-between; 5 | min-height: 50vh; 6 | border-radius: 0 0 3px 3px; 7 | border-bottom-left-radius: 4px; 8 | border-bottom-right-radius: 4px; 9 | } 10 | 11 | .FanBody.withStreams { 12 | max-height: 70vh; 13 | } 14 | 15 | .CelebrityHostBody .VideoWrap { 16 | flex: 1; 17 | align-items: center; 18 | border: 1px solid #000000; 19 | min-height: 50vh; 20 | } 21 | 22 | .CelebrityHostBody .VideoWrap.hide { 23 | display:none; 24 | } 25 | 26 | .CelebrityHostBody .closeImageHolder { 27 | width: 100%; 28 | border: 1px solid #ddd; 29 | border-bottom-left-radius: 4px; 30 | border-bottom-right-radius: 4px; 31 | background: #FFF; 32 | min-height: 200px; 33 | } 34 | 35 | .CelebrityHostBody .closeImageHolder img { 36 | width: 100%; 37 | } 38 | 39 | .CelebrityHostBody .producerContainer { 40 | position: absolute; 41 | z-index: -100; 42 | visibility: hidden; 43 | } 44 | -------------------------------------------------------------------------------- /flowtypes/alert.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* eslint no-undef: "off" */ 3 | /* beautify preserve:start */ 4 | declare type AlertState = { 5 | show: boolean, 6 | type: AlertType, 7 | title: string, 8 | text: string, 9 | onConfirm: null | (*) => void, 10 | showCancelButton: boolean 11 | } 12 | 13 | declare type AlertOptions = { 14 | show: boolean, 15 | type: AlertType, 16 | title?: string, 17 | text?: string, 18 | onConfirm?: (*) => void, 19 | onCancel?: Unit, 20 | showConfirmButton?: boolean, 21 | showCancelButton?: boolean, 22 | html?: string, 23 | inputPlaceholder?: string, 24 | allowEscapeKey?: boolean 25 | }; 26 | 27 | declare type AlertPartialOptions = { 28 | title?: string, 29 | text?: string, 30 | onConfirm?: (*) => void, 31 | onCancel?: Unit, 32 | showConfirmButton?: boolean, 33 | showCancelButton?: boolean 34 | }; 35 | 36 | declare type AlertType = 'warning' | 'error' | 'success' | 'info' | null; 37 | 38 | declare type AlertAction = 39 | { type: 'RESET_ALERT' } | 40 | { type: 'SET_ALERT', options: AlertState }; 41 | -------------------------------------------------------------------------------- /src/components/Common/NetworkReconnect.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import R from 'ramda'; 4 | import { connect } from 'react-redux'; 5 | import './NetworkReconnect.css'; 6 | 7 | type Props = { 8 | broadcast: BroadcastState 9 | }; 10 | 11 | const ReconnectionOverlay = (): ReactComponent => 12 |
13 |
14 |
Attempting to reconnect you...
15 |
16 |
17 |
; 18 | 19 | const NetworkReconnect = (props: Props): ReactComponent => { 20 | const { reconnecting, disconnected } = props.broadcast; 21 | const shouldDisplay = reconnecting && !disconnected; 22 | return ( 23 |
24 | { shouldDisplay && } 25 |
26 | ); 27 | }; 28 | 29 | const mapStateToProps = (state: State): Props => ({ 30 | broadcast: R.prop('broadcast', state), 31 | }); 32 | 33 | export default connect(mapStateToProps, null)(NetworkReconnect); 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Andrea Phillips 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 | 23 | -------------------------------------------------------------------------------- /src/services/util.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import R from 'ramda'; 3 | 4 | const properCase = (input: string): string => `${R.toUpper(R.head(input))}${R.tail(input)}`; 5 | 6 | // Get the fan type based on their status 7 | const fanTypeByStatus = (status: FanStatus): FanType => { 8 | switch (status) { 9 | case 'inLine': 10 | return 'activeFan'; 11 | case 'backstage': 12 | return 'backstageFan'; 13 | case 'stage': 14 | return 'fan'; 15 | default: 16 | return 'activeFan'; 17 | } 18 | }; 19 | 20 | // Get the fan type based on their active fan record 21 | const fanTypeForActiveFan = (fan: ActiveFan): FanType => { 22 | if (fan.isBackstage) { 23 | return 'backstageFan'; 24 | } else if (fan.isOnStage) { 25 | return 'fan'; 26 | } 27 | return 'activeFan'; 28 | }; 29 | 30 | 31 | const isFan = (type: UserRole | 'activeFan'): boolean => R.contains(type, ['fan', 'backstageFan', 'activeFan']); 32 | 33 | const isUserOnStage = (user: ParticipantType | 'activeFan'): boolean => R.contains(user, ['fan', 'host', 'celebrity']); 34 | 35 | module.exports = { 36 | fanTypeForActiveFan, 37 | fanTypeByStatus, 38 | isFan, 39 | isUserOnStage, 40 | properCase, 41 | }; 42 | -------------------------------------------------------------------------------- /src/components/Common/NetworkReconnect.css: -------------------------------------------------------------------------------- 1 | .ReconnectionOverlay { 2 | background-color: black; 3 | -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=40)"; 4 | background-color: rgba(0, 0, 0, 0.4); 5 | position: fixed; 6 | left: 0; 7 | right: 0; 8 | top: 0; 9 | bottom: 0; 10 | display: flex; 11 | justify-content: center; 12 | align-items: center; 13 | z-index: 10000; 14 | } 15 | 16 | .ReconnectionOverlay .ReconnectionMask { 17 | width: 326px; 18 | height: 165px; 19 | border-radius: 8px; 20 | background-color: #e1a42e; 21 | min-height: 165px !important; 22 | } 23 | 24 | .ReconnectionOverlay .ReconnectionMask .ReconnectionText { 25 | font-family: Helvetica; 26 | font-size: 17.4px; 27 | font-weight: normal; 28 | font-style: normal; 29 | font-stretch: normal; 30 | line-height: normal; 31 | letter-spacing: normal; 32 | text-align: center; 33 | color: #ffffff; 34 | margin-top: 20px; 35 | } 36 | 37 | .ReconnectionOverlay .ReconnectionMask .ReconnectionIcon { 38 | margin-left: 48px; 39 | margin-top: 30px; 40 | background-image: url(images/reconnection-icon.png); 41 | background-repeat: no-repeat; 42 | width: 240px; 43 | height: 61px; 44 | } 45 | -------------------------------------------------------------------------------- /src/services/eventUrls.js: -------------------------------------------------------------------------------- 1 | import R from 'ramda'; 2 | import { remove as removeDiacritics } from 'diacritics'; 3 | import Hashids from 'hashids'; 4 | 5 | // eslint-disable-next-line no-regex-spaces 6 | const convertName = R.compose(R.replace(/ /g, '-'), R.toLower, R.replace(/ +/g, ' '), removeDiacritics, R.replace(/[^a-zA-Z0-9 ]/g, ''), R.trim); 7 | const origin = (): string => window.location.origin; 8 | const hashEventName = (name: string): string => R.isEmpty(name) ? '' : new Hashids(name).encode(1, 2, 3); 9 | 10 | // We still need to return urls when no name is provided as it provides the base for the urls in the event form 11 | const createUrls = ({ name, adminId }: { name?: string, adminId: string}): EventUrls => { 12 | if (!adminId) { return {}; } 13 | const base = origin(); 14 | const eventName = convertName(name); 15 | const eventNameHash = hashEventName(eventName); 16 | return { 17 | fanUrl: `${base}/show/${adminId}/${eventName}`, 18 | fanAudioUrl: `${base}/post-production/${adminId}/${eventName}`, 19 | hostUrl: `${base}/show-host/${adminId}/${eventNameHash}`, 20 | celebrityUrl: `${base}/show-celebrity/${adminId}/${eventNameHash}`, 21 | }; 22 | }; 23 | 24 | export default createUrls; 25 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 20 | IBS 21 | 22 | 23 |
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/components/Logout/Logout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import R from 'ramda'; 3 | import { connect } from 'react-redux'; 4 | import { withRouter, Link } from 'react-router'; 5 | import Icon from 'react-fontawesome'; 6 | import { signOut } from '../../actions/auth'; 7 | import './Logout.css'; 8 | 9 | /* beautify preserve:start */ 10 | type BaseProps = { currentUser: User }; 11 | type DispatchProps = { logOutUser: Unit }; 12 | type Props = BaseProps & DispatchProps; 13 | /* beautify preserve:end */ 14 | 15 | const Logout = ({ currentUser, logOutUser }: Props): ReactElement => 16 | currentUser && 17 | 18 | {currentUser.displayName} 19 | | 20 | 21 | ; 22 | 23 | const mapStateToProps = (state: { currentUser: User }): Props => R.pick(['currentUser'], state); 24 | const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch): DispatchProps => 25 | ({ 26 | logOutUser: () => { 27 | dispatch(signOut()); 28 | }, 29 | }); 30 | 31 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Logout)); 32 | -------------------------------------------------------------------------------- /src/components/Broadcast/Fan/components/FanStatusBar.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import classNames from 'classnames'; 4 | import './FanStatusBar.css'; 5 | 6 | type Props = { 7 | fanStatus: FanStatus 8 | }; 9 | 10 | const FanStatusBar = (props: Props): ReactComponent => { 11 | const { fanStatus } = props; 12 | let statusText = ''; 13 | let statusClass = ''; 14 | switch (fanStatus) { 15 | case 'connecting': 16 | statusText = 'Connecting ...'; 17 | statusClass = 'lightBlue'; 18 | break; 19 | case 'disconnecting': 20 | statusText = 'Leaving The Line...'; 21 | statusClass = 'lightBlue'; 22 | break; 23 | case 'inLine': 24 | statusText = 'You Are In Line'; 25 | statusClass = 'lightBlue'; 26 | break; 27 | case 'backstage': 28 | statusText = 'You Are In Backstage'; 29 | statusClass = 'blue'; 30 | break; 31 | case 'stage': 32 | statusText = 'You Are On Stage'; 33 | statusClass = 'green'; 34 | break; 35 | default: 36 | statusText = ''; 37 | break; 38 | } 39 | const classes = classNames('FanStatusBar', statusClass); 40 | if (!statusText) return
; 41 | return (
{statusText}
); 42 | }; 43 | 44 | export default FanStatusBar; 45 | -------------------------------------------------------------------------------- /src/components/Dashboard/components/EventList.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import R from 'ramda'; 4 | import classNames from 'classnames'; 5 | import EventActions from './EventActions'; 6 | import { eventName, eventTime, eventStatus } from './EventListHelpers'; 7 | import './EventList.css'; 8 | 9 | const renderEvent = (e: BroadcastEvent): ReactComponent => 10 |
  • 11 |
    12 |
    13 | { eventName(e) } 14 |
    15 |
    16 | { eventTime(e) } 17 |
    18 |
    19 | {eventStatus(e).text} 20 |
    21 |
    22 | 23 |
  • ; 24 | 25 | type Props = { events: BroadcastEvent[] }; 26 | const EventList = ({ events }: Props): ReactComponent => 27 |
      28 | { 29 | R.ifElse( 30 | R.isEmpty, 31 | (): ReactComponent =>
      No events available
      , 32 | R.map(renderEvent) // eslint-disable-line comma-dangle 33 | )(events) 34 | } 35 |
    ; 36 | 37 | export default EventList; 38 | -------------------------------------------------------------------------------- /src/components/Broadcast/CelebrityHost/components/CelebrityHostHeader.css: -------------------------------------------------------------------------------- 1 | .CelebrityHostHeader { 2 | box-shadow: none; 3 | background: #fff; 4 | padding: 20px; 5 | position: relative; 6 | padding: 0; 7 | border-radius: 4px 4px 0 0; 8 | min-height: 90px; 9 | border-bottom: 0; 10 | display: flex; 11 | flex-direction: column; 12 | } 13 | 14 | .CelebrityHostHeader-main { 15 | display: flex; 16 | justify-content: space-between; 17 | padding: 8px 16px; 18 | font-family: 'Montserrat', sans-serif; 19 | } 20 | 21 | .CelebrityHostHeader-main h4 { 22 | margin: 0; 23 | width: 100%; 24 | text-align: left; 25 | font-size: 1.5em; 26 | font-family: inherit; 27 | font-weight: 400; 28 | line-height: 1.6; 29 | color: #37363E; 30 | text-transform: uppercase; 31 | } 32 | 33 | .CelebrityHostHeader-main h4 sup { 34 | position: relative; 35 | font-size: 75%; 36 | line-height: 0; 37 | vertical-align: baseline; 38 | top: -.5em; 39 | margin-left:3px; 40 | } 41 | 42 | .CelebrityHostHeader-main .user-role { 43 | text-transform: uppercase; 44 | font-weight: 600; 45 | color: #00A2E3; 46 | } 47 | 48 | .CelebrityHostHeader-notice { 49 | width: 100%; 50 | background: #EF4836; 51 | color: white; 52 | opacity: 0; 53 | transition: opacity 1s ease-in-out; 54 | } 55 | 56 | .CelebrityHostHeader-notice.active { 57 | opacity: 1; 58 | } 59 | -------------------------------------------------------------------------------- /src/services/eventFiltering.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import R from 'ramda'; 3 | 4 | type FilterInput = { 5 | status: EventStatus, 6 | archiveId: string 7 | }; 8 | 9 | const statusFilter = (filter: EventFilter): (FilterInput => boolean) => ({ status, archiveId = '' }: FilterInput): boolean => { 10 | switch (filter) { 11 | case 'all': 12 | return true; 13 | case 'current': 14 | return status !== 'closed'; 15 | case 'archived': 16 | return status === 'closed' && !!archiveId.length; 17 | default: 18 | return true; 19 | } 20 | }; 21 | 22 | type Comparator = (a: BroadcastEvent, b: BroadcastEvent) => number; 23 | const comparator = (sorting: EventSorting): Comparator => { 24 | 25 | const operator = sorting.order === 'descending' ? 'gt' : 'lt'; 26 | const sortByTypeToProp: {[type: EventSortByOption]: string} = { 27 | mostRecent: 'updatedAt', 28 | startDate: 'dateTimeStart', 29 | state: 'status', 30 | }; 31 | 32 | const sortingProp = R.prop(sortByTypeToProp[sorting.sortBy]); 33 | return R.comparator((a: BroadcastEvent, b: BroadcastEvent): number => R[operator](sortingProp(a), sortingProp(b))); 34 | }; 35 | 36 | const filterAndSort = ({ map, filter, sorting }: EventsState): BroadcastEvent[] => 37 | R.compose( 38 | R.sort(comparator(sorting)), 39 | R.filter(statusFilter(filter)), 40 | )(R.values(map)); 41 | 42 | module.exports = { 43 | filterAndSort, 44 | }; 45 | -------------------------------------------------------------------------------- /src/components/Users/components/UserActions.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import R from 'ramda'; 4 | import { connect } from 'react-redux'; 5 | import { withRouter } from 'react-router'; 6 | import Icon from 'react-fontawesome'; 7 | import { deleteUser as removeUser } from '../../../actions/users'; 8 | import './UserActions.css'; 9 | 10 | /** Event Actions */ 11 | type BaseProps = { user: User, toggleEditPanel: Unit }; 12 | type DispatchProps = { deleteUser: UserId => void }; 13 | type InitialProps = { adminId: string }; 14 | type Props = BaseProps & DispatchProps & InitialProps; 15 | const UserActions = ({ user, deleteUser, toggleEditPanel, adminId }: Props): ReactComponent => 16 |
    17 | 18 | { !adminId && } 19 |
    ; 20 | 21 | const mapStateToProps = (state: State, ownProps: InitialProps): BaseProps => ({ 22 | adminId: R.path(['params', 'adminId'], ownProps), 23 | }); 24 | 25 | const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch): DispatchProps => 26 | ({ 27 | deleteUser: (userId: UserId) => { 28 | dispatch(removeUser(userId)); 29 | }, 30 | }); 31 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(UserActions)); 32 | -------------------------------------------------------------------------------- /src/components/Dashboard/Dashboard.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import R from 'ramda'; 4 | import { connect } from 'react-redux'; 5 | import { getBroadcastEvents } from '../../actions/events'; 6 | import DashboardHeader from './components/DashboardHeader'; 7 | import DashboardEvents from './components/DashboardEvents'; 8 | import './Dashboard.css'; 9 | 10 | /* beautify preserve:start */ 11 | type BaseProps = { currentUser: User, events: EventsState }; 12 | type DispatchProps = { loadEvents: UserId => void }; 13 | type Props = BaseProps & DispatchProps; 14 | /* beautify preserve:end */ 15 | 16 | class Dashboard extends Component { 17 | props: Props; 18 | 19 | componentDidMount() { 20 | const { currentUser, loadEvents } = this.props; 21 | loadEvents(currentUser.id); 22 | } 23 | 24 | render(): ReactComponent { 25 | return ( 26 |
    27 | 28 | 29 |
    30 | ); 31 | } 32 | } 33 | 34 | 35 | const mapStateToProps = (state: State): BaseProps => R.pick(['currentUser', 'events'], state); 36 | const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch): DispatchProps => 37 | ({ 38 | loadEvents: (userId: string) => { 39 | dispatch(getBroadcastEvents(userId, '/admin')); 40 | }, 41 | }); 42 | export default connect(mapStateToProps, mapDispatchToProps)(Dashboard); 43 | -------------------------------------------------------------------------------- /src/reducers/fan.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import R from 'ramda'; 3 | 4 | const initialState = (): FanState => ({ 5 | ableToJoin: false, 6 | fanName: '', 7 | fitMode: 'contain', 8 | status: 'disconnected', 9 | inPrivateCall: false, 10 | postProduction: R.startsWith('/post-production/', window.location.pathname), 11 | networkTest: { 12 | interval: null, 13 | timeout: null, 14 | }, 15 | publisherMinimized: false, 16 | }); 17 | 18 | const fan = (state: FanState = initialState(), action: FanAction): FanState => { 19 | switch (action.type) { 20 | case 'SET_FITMODE': 21 | return R.assoc('fitMode', action.fitMode, state); 22 | case 'SET_FAN_STATUS': 23 | return R.assoc('status', action.status, state); 24 | case 'SET_FAN_PRIVATE_CALL': 25 | return R.assoc('inPrivateCall', action.inPrivateCall, state); 26 | case 'SET_FAN_NAME': 27 | return R.assoc('fanName', action.fanName, state); 28 | case 'SET_ABLE_TO_JOIN': 29 | return R.assoc('ableToJoin', action.ableToJoin, state); 30 | case 'SET_NETWORK_TEST_INTERVAL': 31 | return R.assocPath(['networkTest', 'interval'], action.interval, state); 32 | case 'SET_NETWORK_TEST_TIMEOUT': 33 | return R.assocPath(['networkTest', 'timeout'], action.timeout, state); 34 | case 'SET_PUBLISHER_MINIMIZED': 35 | return R.assoc('publisherMinimized', action.minimized, state); 36 | default: 37 | return state; 38 | } 39 | }; 40 | 41 | export default fan; 42 | -------------------------------------------------------------------------------- /src/components/Broadcast/Producer/components/ProducerChat.css: -------------------------------------------------------------------------------- 1 | .ProducerChat { 2 | position: fixed; 3 | bottom: 0; 4 | display: flex; 5 | align-items: flex-end; 6 | } 7 | 8 | .ProducerChat .Chat { 9 | margin: 0 5px; 10 | } 11 | 12 | .ProducerChat .Chat.host .ChatHeader, 13 | .ProducerChat .Chat.host .ChatForm button { 14 | background-color: #f15949; 15 | } 16 | 17 | .ProducerChat .Chat.celebrity .ChatHeader, 18 | .ProducerChat .Chat.celebrity .ChatForm button { 19 | background-color: #a27ae8; 20 | } 21 | 22 | .Active-Fan-Chats { 23 | width: 260px; 24 | } 25 | 26 | .Active-Fan-Chats .btn.toggle-list { 27 | width: 100%; 28 | } 29 | 30 | .Active-Fan-Chats .btn.toggle-list .icon { 31 | margin-right: 5px; 32 | } 33 | 34 | .Active-Fan-Chats .non-active-list-container { 35 | margin-left: 5px; 36 | width: 100%; 37 | } 38 | 39 | .Active-Fan-Chats ul.non-active-list { 40 | list-style-type: none; 41 | margin: 0; 42 | padding: 0; 43 | width: 100%; 44 | } 45 | 46 | .Active-Fan-Chats ul.non-active-list li { 47 | width: 100%; 48 | color: #00a3e3; 49 | background-color: white; 50 | border: 1px 0px 1px solid lightgrey; 51 | } 52 | 53 | .Active-Fan-Chats ul.non-active-list li button { 54 | display: flex; 55 | width: 100%; 56 | justify-content: center; 57 | align-items: center; 58 | padding: 5px 35px; 59 | } 60 | 61 | .Active-Fan-Chats ul.non-active-list li button .icon { 62 | margin-right: 5px; 63 | } -------------------------------------------------------------------------------- /src/components/Dashboard/components/FilterEvents.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import R from 'ramda'; 4 | import { connect } from 'react-redux'; 5 | import classNames from 'classnames'; 6 | import { filterBroadcastEvents } from '../../../actions/events'; 7 | 8 | type BaseProps = { 9 | filter: EventFilter 10 | }; 11 | type DispatchProps = { 12 | setFilter: EventFilter => void 13 | }; 14 | type Props = BaseProps & DispatchProps; 15 | 16 | const FilterEvents = ({ filter, setFilter }: Props): ReactComponent => 17 |
    18 | 21 | 24 | 27 |
    ; 28 | 29 | 30 | const mapStateToProps = (state: State): Props => R.pick(['filter'], R.prop('events', state)); 31 | const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch): DispatchProps => 32 | ({ 33 | setFilter: (filter: EventFilter) => { 34 | dispatch(filterBroadcastEvents(filter)); 35 | }, 36 | }); 37 | 38 | export default connect(mapStateToProps, mapDispatchToProps)(FilterEvents); 39 | 40 | -------------------------------------------------------------------------------- /src/components/Alert/Alert.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import R from 'ramda'; 4 | import { connect } from 'react-redux'; 5 | import SweetAlert from 'sweetalert-react'; 6 | import 'sweetalert/dist/sweetalert.css'; 7 | import { resetAlert } from '../../actions/alert'; 8 | 9 | type DispatchProps = { reset: Unit }; 10 | type Props = AlertOptions & DispatchProps; 11 | const Alert = (props: Props): ReactComponent => { 12 | const { show, type, title, text, showConfirmButton = true, showCancelButton } = props; 13 | const { onConfirm, onCancel, reset, html = false, inputPlaceholder = '', allowEscapeKey = true } = props; 14 | const onEscapeKey = allowEscapeKey ? reset : (): boolean => false; 15 | return ( 16 |
    17 | 30 |
    31 | ); 32 | }; 33 | 34 | const mapStateToProps = (state: State): Props => R.prop(['alert'], state); 35 | const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch): DispatchProps => 36 | ({ 37 | reset: () => { 38 | dispatch(resetAlert()); 39 | }, 40 | }); 41 | export default connect(mapStateToProps, mapDispatchToProps)(Alert); 42 | -------------------------------------------------------------------------------- /src/components/Broadcast/Fan/components/FanHLSPlayer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import scriptLoader from 'react-async-script-loader'; 4 | import './FanHLSPlayer.css'; 5 | 6 | type Props = { 7 | hlsUrl: string, 8 | isLive: EventStatus 9 | }; 10 | 11 | class FanHLSPlayer extends Component { 12 | props: Props; 13 | start: Unit; 14 | 15 | componentDidMount() { 16 | const { isLive } = this.props; 17 | if (isLive) this.start(); 18 | } 19 | 20 | componentWillReceiveProps() { 21 | const { isLive } = this.props; 22 | if (isLive && !window.flowplayer) this.start(); 23 | } 24 | 25 | start() { 26 | const { hlsUrl } = this.props; 27 | setTimeout(() => { 28 | window.flowplayer('#hlsjslive', { 29 | splash: false, 30 | embed: false, 31 | ratio: 9 / 16, 32 | autoplay: true, 33 | clip: { 34 | autoplay: true, 35 | live: true, 36 | sources: [{ 37 | type: 'application/x-mpegurl', 38 | src: hlsUrl, 39 | }], 40 | }, 41 | }); 42 | }, 1000); 43 | } 44 | 45 | render(): ReactComponent { 46 | return ( 47 |
    48 |
    49 |
    ); 50 | } 51 | } 52 | 53 | export default scriptLoader( 54 | [ 55 | 'https://releases.flowplayer.org/7.2.1/flowplayer.min.js', 56 | 'https://releases.flowplayer.org/hlsjs/flowplayer.hlsjs.min.js', 57 | ], 58 | '/assets/bootstrap-markdown.js', 59 | )(FanHLSPlayer); 60 | -------------------------------------------------------------------------------- /flowtypes/user.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* eslint no-undef: "off" */ 3 | /* beautify preserve:start */ 4 | 5 | declare type UserId = string; 6 | declare type User = { 7 | id: UserId, 8 | displayName: string, 9 | email: string, 10 | otApiKey: string, 11 | otSecret: string, 12 | superAdmin: boolean, 13 | hls: boolean, 14 | httpSupport: boolean 15 | }; 16 | 17 | declare type HostCelebCredentials = { 18 | apiKey: string, 19 | stageToken: string, 20 | httpSupport: boolean 21 | }; 22 | 23 | declare type HostCelebEventData = BroadcastEvent & HostCelebCredentials; 24 | 25 | declare type UserCredentials = { 26 | apiKey: string, 27 | backstageToken: string, 28 | stageToken: string, 29 | stageSessionId: string, 30 | sessionId: string 31 | }; 32 | 33 | declare type HostCeleb = 'host' | 'celebrity'; 34 | declare type Fan = 'fan' | 'backstageFan'; 35 | declare type UserRole = 'producer' | HostCeleb | Fan; 36 | 37 | declare type UserMap = {[id: UserId]: User}; 38 | declare type CurrentUserState = null | User; 39 | declare type UserAction = 40 | { type: 'SET_CURRENT_USER', user: User } | 41 | { type: 'LOGIN', userId: UserId } | 42 | { type: 'LOGOUT' }; 43 | 44 | 45 | declare type ManageUsersAction = 46 | { type: 'SET_USERS', users: UserMap } | 47 | { type: 'UPDATE_USER', user: User } | 48 | { type: 'REMOVE_USER', userId: UserId }; 49 | 50 | 51 | declare type UserFormData = { 52 | displayName: string, 53 | email: string, 54 | otApiKey: string, 55 | otSecret: string, 56 | hls: boolean, 57 | httpSupport: boolean 58 | }; 59 | 60 | declare type UserUpdateFormData = { id: UserId } & UserFormData; 61 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 0.10 5 | 6 | addons: 7 | sauce_connect: true 8 | 9 | before_install: 10 | - npm install -g grunt-cli jest-cli protractor 11 | 12 | install: npm install 13 | 14 | before_script: grunt build 15 | 16 | cache: 17 | directories: 18 | - node_modules 19 | 20 | env: 21 | global: 22 | - NODE_ENV=test 23 | - secure: EGBb2VT05HQRKqYpB5zLtLcdV3b0IlL4MxoFoybGkNjUxSWSga5pvchMuan7rwnHbR7P0f2v+aY5eC/oVuaavTgfZHP6/I4q1Z0Y8nudCwOWSjwB13XdN/PDc4wpBd+CsV9zeObghbbG3pExiSoWCgx/LqFRaQEIXL8ri8xRJeRN8IFGS837TIESOSHMW5+UTgrwVuuGa5ctQ0MuQ6NxA5skxH5wlHA5j38/Hhm4Xyn8rNRTZTxxf+2CYumsAKKqvgu4sB7q/Ni1CMxUHPuNdaaTd0VK+/MqKIXGUsfCaeYlDVoIDjQ4M0iDAJ8tUUDdiDw4wbXxUVXU16MYxc+w5VDx8Jq8S+K2oz+NyPP8IstW1csCntq6oOJiJJAUPO/P0e4OB66V9/DR+FvlUhZ88sO+oAuS1crA/rlgu8JyVk4ceZkMxDG7i0pMlpaMXb9nhFdonuZnCJ+ikmXVfDZw1KGvd4f4pcC2UaceH8GlCGidIwQdrOxEzqhgQO2XJ8k5JtiBLdr+rdP0uGmw1hK3ltbJwkbhM6/bYzY4V/2PIkRejQ2uN68vB2dFLqhynjzVhqTuEmzzrjnQBy3b6FAcZaZXQ90BWqvBSuKAJ8I7pYJZSmZY+QjD2lvWdczKErLLrH2S2ztA+pc4xnNx0aylqBU4OjAkfFq9MpRn1JZJsQI= 24 | - secure: bsOrhDf8FYJF3Q2nFOrtbRmmJFqWhuG1RQqQWY1KAXHH/0w2VpCgk6zingNryIgrTNuBnq0hyZ1U1AgfyMNZw9oT5kvpE6ndk4SI4ajKSTJ87Lwj+78jKR+Dv19D/d/J5SvfvWuG2RfuXOUQ2qP5XZRJlWZLN56TVi26AV4E28jRmCAw4ktmr/UycooUcItxDYkm1+nFJznvtH3zcwtaRk8wNNCs0PlSmw7OsvQmqSF0vknzsq2opqD126PPe+7fhr0sdtYQX1Bdrdnm0xFb9VSon0Vuqdlv22/N+elPse11novhm/rDqRbhutcZ3+S1pdYTJ3hfEeiMRuNC6ouDjyNvnvdlfgdqG6s8c+Fw577A0FQggCve2KBf7ZS3g5b3jrFVQkB5JIRKV1xpq2kLMV+VK+B1L45mHjQ8PYDIMzfOu/5KRPSyeJKirpkWnCqAoyruyTx8INUU+g6/B+i9uXXgSvpsDqjctigEEKk8zXd3UPXTRWUNR/vz4LTytHfOAkDgXBH98IuYRpraAkvgfyeXhOeKS4AS6KbExjmQhAbHQ+6GpJW7DiaE50zovg8UPa4/4UFyjRxniMMBBGs1kSrQ7hRAac6vx8pBt43sTwyGsZLj48ZI+mzZojle+CS5i94qa2JDMCtsrx0HVx2MOEja5ivR3u84mKKQjDGjFgw= 25 | -------------------------------------------------------------------------------- /src/components/Dashboard/components/DashboardEvents.css: -------------------------------------------------------------------------------- 1 | .DashboardEvents-controls { 2 | display: flex; 3 | justify-content: space-between; 4 | align-items: center; 5 | } 6 | 7 | .DashboardEvents-controls .filter-events, 8 | .DashboardEvents-controls .sort-events { 9 | display: flex; 10 | align-items: center; 11 | } 12 | 13 | .DashboardEvents-controls .sort-events { 14 | padding: 0 5px; 15 | } 16 | 17 | .DashboardEvents-controls .filter-events .btn, 18 | .DashboardEvents-controls .sort-events .btn { 19 | width: 120px; 20 | height: 30px; 21 | margin: 0 5px; 22 | font-size: 13px; 23 | } 24 | 25 | .DashboardEvents-controls .sort-events .sort-events-label { 26 | margin-right: 5px; 27 | color: #888; 28 | font-size: 13px; 29 | } 30 | 31 | .DashboardEvents-controls .sort-events .btn { 32 | display: flex; 33 | margin: 0; 34 | border: none; 35 | background-color: #E4E4E4; 36 | color: #58666e; 37 | } 38 | 39 | .DashboardEvents-controls .sort-events button:nth-of-type(1) { 40 | border-top-right-radius: 0px; 41 | border-bottom-right-radius: 0px; 42 | } 43 | 44 | .DashboardEvents-controls .sort-events button:nth-of-type(2) { 45 | border-left: 1px solid #ccc; 46 | border-right: 1px solid #ccc; 47 | border-radius: 0px; 48 | } 49 | 50 | .DashboardEvents-controls .sort-events button:nth-of-type(3) { 51 | border-top-left-radius: 0px; 52 | border-bottom-left-radius: 0px; 53 | } 54 | 55 | .DashboardEvents-controls .sort-events .btn .fa { 56 | display: none; 57 | margin: 1px 3px 0 0; 58 | } 59 | 60 | .DashboardEvents-controls .sort-events .btn.active { 61 | color: white; 62 | background-color: #46b8da; 63 | } 64 | 65 | .DashboardEvents-controls .sort-events .btn.active .fa { 66 | display: initial; 67 | } -------------------------------------------------------------------------------- /src/components/Dashboard/components/SortEvents.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import R from 'ramda'; 4 | import { connect } from 'react-redux'; 5 | import classNames from 'classnames'; 6 | import Icon from 'react-fontawesome'; 7 | import { sortBroadcastEvents } from '../../../actions/events'; 8 | 9 | type BaseProps = { 10 | sorting: EventSorting 11 | }; 12 | type DispatchProps = { 13 | setSorting: EventSorting => void 14 | }; 15 | type Props = BaseProps & DispatchProps; 16 | 17 | const SortEvents = ({ sorting, setSorting }: Props): ReactComponent => { 18 | const { order, sortBy } = sorting; 19 | const iconName = order === 'descending' ? 'arrow-down' : 'arrow-up'; 20 | return ( 21 |
    22 | Sort by: 23 | 26 | 29 | 32 |
    33 | ); 34 | }; 35 | 36 | const mapStateToProps = (state: State): Props => R.pick(['sorting'], R.prop('events', state)); 37 | const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch): DispatchProps => 38 | ({ 39 | setSorting: (sorting: EventSorting) => { 40 | dispatch(sortBroadcastEvents(sorting)); 41 | }, 42 | }); 43 | 44 | export default connect(mapStateToProps, mapDispatchToProps)(SortEvents); 45 | -------------------------------------------------------------------------------- /src/components/Broadcast/CelebrityHost/components/CelebrityHostBody.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import classNames from 'classnames'; 4 | import VideoHolder from '../../../Common/VideoHolder'; 5 | import './CelebrityHostBody.css'; 6 | import defaultImg from '../../../../images/TAB_VIDEO_PREVIEW_LS.jpg'; 7 | 8 | const userTypes: ParticipantType[] = ['host', 'celebrity', 'fan']; 9 | 10 | type Props = { 11 | status: EventStatus, 12 | endImage?: EventImage, 13 | participants: null | BroadcastParticipants, // publishOnly => null 14 | userType: 'host' | 'celebrity', 15 | eventStarted: boolean, 16 | startEvent: Unit 17 | }; 18 | const CelebrityHostBody = (props: Props): ReactComponent => { 19 | const { status, endImage, participants, userType, eventStarted, startEvent } = props; 20 | const isClosed = status === 'closed'; 21 | const imgClass = classNames('CelebrityHostBody', { withStreams: !isClosed, notStarted: !eventStarted }); 22 | const endImageUrl = endImage ? endImage.url : null; 23 | return ( 24 |
    25 | { isClosed && 26 |
    27 | event ended 28 |
    29 | } 30 | { !isClosed && !eventStarted && } 31 | { !isClosed && userTypes.map((type: ParticipantType): ReactComponent => 32 | )} 38 |
    39 |
    40 | ); 41 | }; 42 | 43 | export default CelebrityHostBody; 44 | -------------------------------------------------------------------------------- /src/reducers/events.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import R from 'ramda'; 3 | 4 | const initialState: EventsState = { 5 | map: null, // Set to null so we know that events haven't been loaded 6 | filter: 'all', 7 | sorting: { sortBy: 'mostRecent', order: 'descending' }, 8 | mostRecent: null, 9 | active: null, 10 | submitting: false, 11 | }; 12 | 13 | // eslint-disable-next-line no-confusing-arrow 14 | const reverseOrder = (current: EventOrderOption): EventOrderOption => current === 'ascending' ? 'descending' : 'ascending'; 15 | 16 | const events = (state: EventsState = initialState, action: EventsAction): EventsState => { 17 | switch (action.type) { 18 | case 'ADD_EVENT': 19 | return R.assocPath(['map', action.event.id], action.event, state); 20 | case 'SET_EVENTS': 21 | return R.assoc('map', action.events, state); 22 | case 'SET_MOST_RECENT_EVENT': 23 | return R.assoc('mostRecent', action.event, state); 24 | case 'UPDATE_EVENT': 25 | return R.assocPath(['map', action.event.id], R.merge(R.pathOr({}, ['map', action.event.id], state), action.event), state); 26 | case 'SUBMIT_FORM_EVENT': 27 | return R.assoc('submitting', action.submitting, state); 28 | case 'REMOVE_EVENT': 29 | return R.assoc('map', R.omit([action.id], state.map), state); 30 | case 'FILTER_EVENTS': 31 | return R.assoc('filter', action.filter, state); 32 | case 'SORT_EVENTS': 33 | // If the sortBy type is the same, we are only changing the order 34 | if (state.sorting.sortBy === action.sortBy) { 35 | return R.assocPath(['sorting', 'order'], reverseOrder(state.sorting.order), state); 36 | } 37 | // When changing the sortBy type, always revert to descending order 38 | return R.assoc('sorting', { sortBy: action.sortBy, order: 'descending' }, state); 39 | default: 40 | return state; 41 | } 42 | }; 43 | 44 | export default events; 45 | -------------------------------------------------------------------------------- /src/components/Users/Users.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import R from 'ramda'; 4 | import { connect } from 'react-redux'; 5 | import { browserHistory, Link } from 'react-router'; 6 | import { getUsers } from '../../actions/users'; 7 | import UserList from './components/UserList'; 8 | import './Users.css'; 9 | 10 | /* beautify preserve:start */ 11 | type InitialProps = { params: { adminId: string } }; 12 | type BaseProps = { 13 | adminId: string, 14 | currentUser: User 15 | }; 16 | type DispatchProps = { loadUsers: Unit }; 17 | type Props = InitialProps & BaseProps & DispatchProps; 18 | 19 | 20 | /* beautify preserve:end */ 21 | 22 | class Users extends Component { 23 | props: Props; 24 | 25 | componentWillMount() { 26 | if (!this.props.currentUser.superAdmin && this.props.currentUser.id !== this.props.adminId) { 27 | browserHistory.replace('/'); 28 | } 29 | } 30 | 31 | componentDidMount() { 32 | this.props.loadUsers(); 33 | } 34 | 35 | render(): ReactComponent { 36 | const { adminId } = this.props; 37 | return ( 38 |
    39 |
    40 | Back to Events 41 |

    { !adminId ? 'Users' : 'My profile' }

    42 |
    43 |
    44 | 45 |
    46 |
    47 | ); 48 | } 49 | } 50 | 51 | const mapStateToProps = (state: State, ownProps: InitialProps): BaseProps => ({ 52 | adminId: R.path(['params', 'adminId'], ownProps), 53 | currentUser: R.path(['currentUser'], state), 54 | }); 55 | 56 | const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch): DispatchProps => 57 | ({ 58 | loadUsers: () => { 59 | dispatch(getUsers()); 60 | }, 61 | }); 62 | export default connect(mapStateToProps, mapDispatchToProps)(Users); 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interactive-broadcasting-client", 3 | "version": "2.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "babel-polyfill": "^6.23.0", 7 | "classnames": "^2.2.5", 8 | "diacritics": "^1.3.0", 9 | "firebase": "^4.1.3", 10 | "hashids": "^1.1.1", 11 | "lodash.throttle": "^4.1.1", 12 | "lodash.truncate": "^4.4.2", 13 | "moment": "^2.17.1", 14 | "opentok-accelerator-core": "^2.0.12", 15 | "opentok-solutions-logging": "^1.0.10", 16 | "platform": "^1.3.4", 17 | "prop-types": "^15.5.8", 18 | "ramda": "^0.24.0", 19 | "react": "^15.4.2", 20 | "react-async-script-loader": "^0.3.0", 21 | "react-copy-to-clipboard": "^4.2.3", 22 | "react-datetime": "^2.8.8", 23 | "react-dom": "^15.4.2", 24 | "react-fontawesome": "^1.5.0", 25 | "react-onclickoutside": "^5.9.0", 26 | "react-redux": "^5.0.3", 27 | "react-redux-toastr": "^4.4.6", 28 | "react-router": "^3.0.2", 29 | "react-sortable-hoc": "^0.6.3", 30 | "react-toastr": "^2.8.2", 31 | "redux": "^3.6.0", 32 | "redux-thunk": "^2.2.0", 33 | "shortid": "^2.2.8", 34 | "sweetalert": "^1.1.3", 35 | "sweetalert-react": "^0.4.9", 36 | "uuid": "^3.1.0" 37 | }, 38 | "devDependencies": { 39 | "babel-eslint": "^7.1.1", 40 | "eslint": "^4.18.2", 41 | "eslint-config-airbnb": "^14.1.0", 42 | "eslint-plugin-flowtype": "^2.30.3", 43 | "eslint-plugin-import": "^2.2.0", 44 | "eslint-plugin-react": "^6.10.0", 45 | "flow-bin": "*", 46 | "opentok-annotation": "^2.0.53", 47 | "opentok-archiving": "^1.0.19", 48 | "opentok-screen-sharing": "^1.0.27", 49 | "opentok-text-chat": "^1.0.29", 50 | "react-scripts": "0.9.4" 51 | }, 52 | "scripts": { 53 | "start": "react-scripts start", 54 | "build": "react-scripts build", 55 | "test": "react-scripts test --env=jsdom", 56 | "eject": "react-scripts eject", 57 | "flow": "flow" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/components/Dashboard/components/EventList.css: -------------------------------------------------------------------------------- 1 | .EventList-empty { 2 | height: 300px; 3 | display: flex; 4 | justify-content: center; 5 | align-items: center; 6 | font-family: 'Montserrat', sans-serif; 7 | font-size: 20px; 8 | font-weight: 300; 9 | color: rgb(88, 102, 110); 10 | } 11 | 12 | .EventList-item .event-info { 13 | display: flex; 14 | align-items: flex-start; 15 | flex-direction: column; 16 | } 17 | 18 | .EventList-item .event-info .event-name>* { 19 | font-size: 18px; 20 | font-family: 'Montserrat', sans-serif; 21 | font-weight: 600; 22 | color: rgb(88, 102, 110); 23 | } 24 | 25 | .EventList-item .event-info .event-name a { 26 | color: #00a3e3; 27 | } 28 | 29 | .EventList-item .event-info .event-time { 30 | display: flex; 31 | flex-direction: column; 32 | align-items: flex-start; 33 | color: #888; 34 | font-size: 13px; 35 | } 36 | 37 | .EventList-item .event-info .event-time .date, 38 | .EventList-item .event-info .event-time .duration { 39 | margin: 2px 0; 40 | } 41 | 42 | .EventList-item .event-info .event-time .duration .fa { 43 | margin-right: 5px; 44 | } 45 | 46 | .EventList-item .event-info .event-status { 47 | font-size: 12px; 48 | border: 2px solid; 49 | border-radius: 3px; 50 | display: flex; 51 | justify-content: center; 52 | align-items: center; 53 | margin-top: 10px; 54 | padding: 2px 14px; 55 | } 56 | 57 | .EventList-item .event-info .event-status.created { 58 | color: #00a3e3; 59 | border-color: #00a3e3; 60 | } 61 | 62 | .EventList-item .event-info .event-status.preshow { 63 | color: #F39C12; 64 | border-color: #F39C12; 65 | } 66 | 67 | .EventList-item .event-info .event-status.live { 68 | color: #26A65B; 69 | border-color: #26A65B; 70 | } 71 | 72 | .EventList-item .event-info .event-status.closed { 73 | color: #E4E4E4; 74 | border-color: #E4E4E4; 75 | } 76 | 77 | .EventList-item .event-actions { 78 | display: flex; 79 | justify-content: center; 80 | align-items: center; 81 | } 82 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "airbnb", 4 | "plugin:flowtype/recommended" 5 | ], 6 | "env": { 7 | "browser": true, 8 | "node": true 9 | }, 10 | "parser": "babel-eslint", 11 | "plugins": [ 12 | "flowtype" 13 | ], 14 | "rules": { 15 | "strict": 0, 16 | "padded-blocks": 0, 17 | "no-unused-expressions": [2, { "allowTernary": true, "allowShortCircuit": true }], 18 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], 19 | "max-len": ["error", 150], 20 | "no-confusing-arrow": 0, 21 | "react/sort-comp": 0, 22 | "react/require-default-props": 0, 23 | "flowtype/boolean-style": [ 24 | 2, 25 | "boolean" 26 | ], 27 | "flowtype/define-flow-type": 1, 28 | "flowtype/delimiter-dangle": [ 29 | 2, 30 | "never" 31 | ], 32 | "flowtype/generic-spacing": [ 33 | 2, 34 | "never" 35 | ], 36 | "flowtype/no-primitive-constructor-types": 2, 37 | "flowtype/no-weak-types": 2, 38 | "flowtype/object-type-delimiter": [ 39 | 2, 40 | "comma" 41 | ], 42 | "flowtype/require-parameter-type": 2, 43 | "flowtype/require-return-type": [ 44 | 2, 45 | "always", 46 | { 47 | "annotateUndefined": "never" 48 | } 49 | ], 50 | "flowtype/require-valid-file-annotation": 2, 51 | "flowtype/semi": [ 52 | 2, 53 | "always" 54 | ], 55 | "flowtype/space-after-type-colon": [ 56 | 2, 57 | "always" 58 | ], 59 | "flowtype/space-before-generic-bracket": [ 60 | 2, 61 | "never" 62 | ], 63 | "flowtype/space-before-type-colon": [ 64 | 2, 65 | "never" 66 | ], 67 | "flowtype/type-id-match": [ 68 | 2, 69 | "^([A-Z][a-zA-Z0-9]+)$" 70 | ], 71 | "flowtype/union-intersection-spacing": [ 72 | 2, 73 | "always" 74 | ], 75 | "flowtype/use-flow-type": 1, 76 | "flowtype/valid-syntax": 1 77 | }, 78 | "settings": { 79 | "flowtype": { 80 | "onlyFilesWithFlowAnnotation": false 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/components/Broadcast/CelebrityHost/CelebrityHost.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #F5F5F5; 3 | color: #58666E; 4 | margin: 0; 5 | line-height: 1.7em; 6 | font-size: 13px; 7 | font-family: 'Open Sans', sans-serif; 8 | outline: 0; 9 | } 10 | 11 | .CelebrityHost { 12 | background-color: #F5F5F5; 13 | min-height: 100%; 14 | padding-left: 30px; 15 | padding-right: 30px; 16 | padding-top: 20px; 17 | color: #58666E; 18 | } 19 | 20 | .CelebrityHost .Container { 21 | padding-right: 15px; 22 | padding-left: 15px; 23 | margin-right: auto; 24 | margin-left: auto; 25 | width: 100%; 26 | position: relative; 27 | min-height: 1px; 28 | } 29 | 30 | .HostCelebChat { 31 | position: fixed; 32 | bottom: 0; 33 | right: 15px; 34 | } 35 | 36 | .CelebrityHostBody.notStarted { 37 | justify-content: center; 38 | align-items: center 39 | } 40 | 41 | .CelebrityHostBody .btn.action.green { 42 | position: absolute; 43 | } 44 | 45 | /* Embed styles */ 46 | .CelebrityHostEmbed { 47 | background: #262626; 48 | padding-left: 0px; 49 | padding-right: 0px; 50 | padding-top: 0px; 51 | } 52 | 53 | .CelebrityHostEmbed .Container { 54 | padding: 0; 55 | } 56 | 57 | .CelebrityHostEmbed .CelebrityHostBody { 58 | height: 76vh; 59 | } 60 | 61 | .CelebrityHostEmbed .CelebrityHostBody .imageHolder { 62 | border: none; 63 | background: #262626; 64 | } 65 | 66 | .CelebrityHostEmbed .CelebrityHostHeader { 67 | border: 1px solid #262626; 68 | } 69 | 70 | .CelebrityHostEmbed .CelebrityHostHeader-main h4 { 71 | color: #FFFFFF; 72 | } 73 | 74 | .CelebrityHostEmbed .CelebrityHostHeader-main { 75 | background: #262626; 76 | padding: 12px 12px; 77 | min-height: 63px; 78 | } 79 | 80 | @media (min-width: 768px) { 81 | .CelebrityHostEmbed .Container { 82 | width: 100%; 83 | } 84 | } 85 | @media (min-width: 992px) { 86 | .CelebrityHostEmbed .Container { 87 | width: 100%; 88 | } 89 | } 90 | @media (min-width: 1200px) { 91 | .CelebrityHostEmbed .Container { 92 | width: 100%; 93 | } 94 | } 95 | 96 | .CelebrityHostEmbed .btn.action { 97 | margin: 0; 98 | } 99 | -------------------------------------------------------------------------------- /src/components/Broadcast/Producer/components/ActiveFanList.css: -------------------------------------------------------------------------------- 1 | .ActiveFanList { 2 | width: 320px; 3 | list-style-type: none; 4 | padding: 10px; 5 | } 6 | 7 | .ActiveFan { 8 | display: flex; 9 | margin: 10px 0; 10 | padding: 5px; 11 | border: 1px solid #BDC4C9; 12 | border-left: 8px solid #BDC4C9; 13 | border-radius: 3px; 14 | background: white; 15 | } 16 | 17 | .ActiveFan.backstage { 18 | border-left-color: #00a3e3; 19 | } 20 | 21 | .ActiveFan.stage { 22 | border-left-color: #26A65B; 23 | } 24 | 25 | .ActiveFan.sortable { 26 | cursor: move; 27 | } 28 | 29 | .ActiveFan .ActiveFanImage { 30 | display: flex; 31 | justify-content: center; 32 | align-items: center; 33 | } 34 | 35 | .ActiveFan .ActiveFanImage img { 36 | height: 50px; 37 | width: 50px; 38 | border-radius: 50%; 39 | } 40 | 41 | .ActiveFan .ActiveFanMain { 42 | display: flex; 43 | flex-direction: column; 44 | flex-grow: 1; 45 | margin-left: 5px; 46 | } 47 | 48 | .ActiveFan .ActiveFanMain .info, 49 | .ActiveFan .ActiveFanMain .actions { 50 | display: flex; 51 | justify-content: space-between; 52 | } 53 | 54 | .ActiveFan .ActiveFanMain .info .name-and-browser { 55 | font-size: 13px; 56 | } 57 | 58 | .ActiveFan .ActiveFanMain .info .connection { 59 | font-size: 11px; 60 | font-weight: 600; 61 | } 62 | 63 | .ActiveFan .ActiveFanMain .info .connection .quality.retrieving { 64 | color: grey; 65 | } 66 | 67 | .ActiveFan .ActiveFanMain .info .connection .quality.poor { 68 | color: #EF4836; 69 | } 70 | 71 | .ActiveFan .ActiveFanMain .info .connection .quality.good { 72 | color: rgba(243, 156, 18, .80); 73 | } 74 | 75 | .ActiveFan .ActiveFanMain .info .connection .quality.great { 76 | color: #30c06c; 77 | } 78 | 79 | .ActiveFan .ActiveFanMain .actions { 80 | display: flex; 81 | justify-content: space-between; 82 | } 83 | 84 | .ActiveFan .ActiveFanMain .actions .btn { 85 | font-size: 12px; 86 | padding: 2px 5px; 87 | } 88 | 89 | .ActiveFan .ActiveFanMain .actions .btn.disabled { 90 | color: lightgray; 91 | } -------------------------------------------------------------------------------- /spotlight-mock-server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const bodyParser = require('body-parser'); 3 | const cors = require('cors') 4 | const app = express(); 5 | app.use(cors()); 6 | app.use(bodyParser.json()) 7 | 8 | app.get('/api/admin/:adminId', (req, res) => { 9 | console.log(req.params) 10 | const user = { 11 | id: '20348', 12 | name: 'Aaron Rice', 13 | email: 'aaron@mail.com', 14 | otAPIKey: '20934809j', 15 | otSecret: '102398432', 16 | superAdmin: true, 17 | httpSupport: false, 18 | }; 19 | res.send(user) 20 | }); 21 | 22 | const events = [ 23 | { 24 | id: '2098r09js', 25 | adminId: '0928402', 26 | adminName: 'Aaron Rice', 27 | archive: true, 28 | archiveId: '0a9udf', 29 | celebrityUrl: 'http://things.com/a09udf', 30 | uncomposed: false, 31 | name: 'tim show', 32 | eventImage: null, 33 | eventEndImage: null, 34 | showStarted: "2017-02-13 12:34:15", 35 | showEnded: "2017-02-21 21:04:57", 36 | status: 'closed' 37 | }, 38 | { 39 | id: '20n092u3r', 40 | adminId: '0928402', 41 | adminName: 'OJ Simpson', 42 | archive: true, 43 | archiveId: '09238908', 44 | celebrityUrl: 'http://things.com/a09udf', 45 | uncomposed: false, 46 | name: 'OJ show', 47 | eventImage: null, 48 | eventEndImage: null, 49 | showStarted: '2017-02-16 11:44:15', 50 | showEnded: '2017-02-20 20:04:57', 51 | status: 'closed', 52 | }, 53 | { 54 | id: '220-3riadf', 55 | adminId: '0928402', 56 | adminName: 'Later today', 57 | archive: false, 58 | celebrityUrl: 'http://things.com/a09udf', 59 | uncomposed: false, 60 | name: 'Get Well Soon', 61 | eventImage: null, 62 | eventEndImage: null, 63 | showStarted: '2017-03-13 20:00:00.000000', 64 | showEnded: null, 65 | status: 'notStarted' 66 | }, 67 | ]; 68 | 69 | 70 | app.get('/api/events/current', (req, res) => { 71 | res.send(events[0]) 72 | }); 73 | 74 | app.get('/api/events', (req, res) => { 75 | res.send(events) 76 | }); 77 | 78 | 79 | app.listen(3001, () => 'Listening on 3001'); 80 | -------------------------------------------------------------------------------- /src/components/Broadcast/Fan/components/FanHeader.css: -------------------------------------------------------------------------------- 1 | .FanHeader { 2 | display: flex; 3 | flex-direction: column; 4 | box-shadow: none; 5 | border: 1px solid #e5e5e5; 6 | background: #fff; 7 | padding: 20px; 8 | position: relative; 9 | padding: 0; 10 | border-radius: 4px 4px 0 0; 11 | border-bottom: 1; 12 | min-height: 60px; 13 | } 14 | 15 | .FanHeader-main { 16 | font-family: 'Montserrat', sans-serif; 17 | color: #58666E; 18 | font-size: 14px; 19 | font-weight: bold; 20 | text-transform: uppercase; 21 | padding: 16px 20px; 22 | background: #fff; 23 | border-top-left-radius: 3px; 24 | border-top-right-radius: 3px; 25 | margin: 0; 26 | padding-top: 9px; 27 | padding-bottom: 12px; 28 | border-bottom: 1px solid #e5e5e5; 29 | float: left; 30 | border-color: #ddd; 31 | border: 1px solid transparent; 32 | display: flex; 33 | justify-content: space-between; 34 | padding: 8px 16px; 35 | font-family: 'Montserrat', sans-serif; 36 | } 37 | 38 | .FanHeader-main h4 { 39 | margin: 0; 40 | width: 100%; 41 | text-align: left; 42 | font-size: 1.5em; 43 | font-family: inherit; 44 | font-weight: 400; 45 | line-height: 1.6; 46 | color: #37363E; 47 | text-transform: uppercase; 48 | } 49 | 50 | .FanHeader-main h4 sup { 51 | position: relative; 52 | font-size: 75%; 53 | line-height: 0; 54 | vertical-align: baseline; 55 | top: -.5em; 56 | margin-left:3px; 57 | } 58 | 59 | .FanHeader-main h4 { 60 | padding-right: 120px; 61 | margin: 0; 62 | text-overflow: ellipsis; 63 | display: inline-block; 64 | vertical-align: top; 65 | white-space: nowrap; 66 | overflow: hidden; 67 | width: 100%; 68 | font-size: 1.5em; 69 | font-family: inherit; 70 | font-weight: 400; 71 | line-height: 1.6; 72 | color: #37363E; 73 | text-transform: uppercase; 74 | } 75 | 76 | .FanHeader-main .btn { 77 | height: 36px; 78 | width: 110px; 79 | } 80 | 81 | .Fan-notice { 82 | width: 100%; 83 | background: #EF4836; 84 | color: white; 85 | opacity: 0; 86 | transition: opacity 1s ease-in-out; 87 | } 88 | 89 | .Fan-notice.active { 90 | opacity: 1; 91 | } 92 | -------------------------------------------------------------------------------- /src/components/Broadcast/Fan/Fan.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #F5F5F5; 3 | color: #58666E; 4 | margin: 0; 5 | line-height: 1.7em; 6 | font-size: 13px; 7 | font-family: 'Open Sans', sans-serif; 8 | outline: 0; 9 | } 10 | 11 | .Fan { 12 | background-color: #F5F5F5; 13 | min-height: 100%; 14 | padding-left: 30px; 15 | padding-right: 30px; 16 | padding-top: 20px; 17 | color: #58666E; 18 | } 19 | 20 | .Fan .Container { 21 | margin-right: auto; 22 | margin-left: auto; 23 | width: 100%; 24 | position: relative; 25 | min-height: 1px; 26 | } 27 | 28 | .FanChat { 29 | position: fixed; 30 | bottom: 0; 31 | right: 15px; 32 | } 33 | 34 | @media (min-width: 768px) { 35 | .Fan .Container { 36 | width: 750px; 37 | } 38 | } 39 | @media (min-width: 992px) { 40 | .Fan .Container { 41 | width: 970px; 42 | } 43 | } 44 | @media (min-width: 1200px) { 45 | .Fan .Container { 46 | width: 1170px; 47 | } 48 | } 49 | 50 | /* Embed styles */ 51 | .FanEmbed.Fan { 52 | background: #262626; 53 | padding-left: 0px; 54 | padding-right: 0px; 55 | padding-top: 0px; 56 | } 57 | .FanEmbed .FanBody { 58 | height: 84vh; 59 | } 60 | 61 | .FanEmbed .FanBody.inLine { 62 | height: 76vh; 63 | } 64 | 65 | .FanEmbed .FanBody .imageHolder { 66 | border: none; 67 | background: #262626; 68 | } 69 | 70 | .FanEmbed .FanHeader { 71 | border: 1px solid #262626; 72 | } 73 | 74 | .FanEmbed .btn.green.getInLine, 75 | .FanEmbed .btn.red.getInLine { 76 | margin-top: 0; 77 | border-color: #262626; 78 | } 79 | 80 | .FanEmbed .FanHeader-main h4 { 81 | color: #FFFFFF; 82 | } 83 | 84 | .FanEmbed .FanHeader-main { 85 | background: #262626; 86 | padding: 12px 12px; 87 | min-height: 63px; 88 | } 89 | 90 | @media (min-width: 768px) { 91 | .FanEmbed .Container { 92 | width: 100%; 93 | } 94 | } 95 | @media (min-width: 992px) { 96 | .FanEmbed .Container { 97 | width: 100%; 98 | } 99 | } 100 | @media (min-width: 1200px) { 101 | .FanEmbed .Container { 102 | width: 100%; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /public/embed.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | var scripts = document.getElementsByTagName('script'); 3 | var parser = new URL(scripts[scripts.length - 1].src); 4 | window.IBSApp = { 5 | defaultConfig: { 6 | container: 'body', 7 | userType: 'fan', 8 | }, 9 | init: function(config) { 10 | /* Replace empty values with values by default */ 11 | config = Object.assign({}, this.defaultConfig, config); 12 | 13 | /* Validate the adminId */ 14 | if (!config.adminId) { 15 | console.error('Error: No adminId provided'); 16 | return; 17 | } 18 | 19 | /* Validate the container */ 20 | var container = document.querySelector(config.container); 21 | if (!container) { 22 | console.error('Error: The container is invalid'); 23 | return; 24 | } 25 | 26 | /* Validate the width */ 27 | if (!config.width) config.width = '600'; 28 | if (isNaN(config.width)) { 29 | console.error('Error: The width is invalid'); 30 | return; 31 | } 32 | 33 | /* Validate the height */ 34 | if (!config.height) config.height = '400'; 35 | if (isNaN(config.height)) { 36 | console.error('Error: The height is invalid'); 37 | return; 38 | } 39 | 40 | /* Create the iframe element */ 41 | var iframe = document.createElement('iframe'); 42 | 43 | /* Set the iframe src */ 44 | if (config.userType === 'fan') { 45 | iframe.src = [parser.origin, 'show', config.adminId].join('/'); 46 | } else if (config.userType === 'celebrity' || config.userType === 'host') { 47 | iframe.src = [parser.origin, '/show-', config.userType, '/', config.adminId].join(''); 48 | } 49 | 50 | if (config.fitMode === 'cover') { 51 | iframe.src += '?fitMode=cover'; 52 | } 53 | 54 | /* Set the rest of the iframe properties */ 55 | iframe.frameBorder = '0'; 56 | iframe.width = config.width; 57 | iframe.height = config.height; 58 | iframe.scrolling = 'no'; 59 | iframe.allow = 'microphone; camera'; 60 | iframe.onload = function() { 61 | iframe.contentWindow.document.body.style.background = '#262626'; 62 | } 63 | /* Append the iframe */ 64 | container.appendChild(iframe); 65 | }, 66 | }; 67 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { Router, Route, IndexRedirect, browserHistory } from 'react-router'; 4 | import App from './components/App/App'; 5 | import Login from './components/Login/Login'; 6 | import Dashboard from './components/Dashboard/Dashboard'; 7 | import Users from './components/Users/Users'; 8 | import UpdateEvent from './components/UpdateEvent/UpdateEvent'; 9 | import ViewEvent from './components/ViewEvent/ViewEvent'; 10 | import Producer from './components/Broadcast/Producer/Producer'; 11 | import AuthRoutes from './components/AuthRoutes/AuthRoutes'; 12 | import CelebrityHost from './components/Broadcast/CelebrityHost/CelebrityHost'; 13 | import Fan from './components/Broadcast/Fan/Fan'; 14 | 15 | const routes = ( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | ); 38 | 39 | export default routes; 40 | -------------------------------------------------------------------------------- /src/components/ViewEvent/ViewEvent.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import R from 'ramda'; 4 | import { connect } from 'react-redux'; 5 | import { withRouter, Link } from 'react-router'; 6 | import { getBroadcastEvents } from '../../actions/events'; 7 | import Loading from '../../components/Common/Loading'; 8 | import defaultImg from '../../images/TAB_VIDEO_PREVIEW_LS.jpg'; 9 | import './ViewEvent.css'; 10 | 11 | /* beautify preserve:start */ 12 | type InitialProps = { params: { id?: EventId } }; 13 | type BaseProps = { 14 | user: CurrentUserState, 15 | events: null | BroadcastEventMap, 16 | eventId: null | EventId 17 | }; 18 | type DispatchProps = { 19 | loadEvents: UserId => void 20 | }; 21 | type Props = InitialProps & BaseProps & DispatchProps; 22 | /* beautify preserve:end */ 23 | 24 | class UpdateEvent extends Component { 25 | props: Props; 26 | 27 | componentDidMount() { 28 | if (!this.props.events) { 29 | this.props.loadEvents(R.path(['user', 'id'], this.props)); 30 | } 31 | } 32 | render(): ReactComponent { 33 | const { eventId } = this.props; 34 | const event = R.pathOr(null, ['events', eventId], this.props); 35 | if (!event) return ; 36 | const poster = R.pathOr(defaultImg, ['startImage', 'url'], event); 37 | return ( 38 |
    39 |
    40 | Back to Events 41 |

    {event.name}

    42 |
    43 |
    44 |
    46 |
    47 | ); 48 | } 49 | } 50 | 51 | const mapStateToProps = (state: State, ownProps: InitialProps): BaseProps => ({ 52 | eventId: R.pathOr(null, ['params', 'id'], ownProps), 53 | events: R.path(['events', 'map'], state), 54 | user: state.currentUser, 55 | state, 56 | }); 57 | 58 | const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ 59 | loadEvents: (userId: UserId) => { 60 | dispatch(getBroadcastEvents(userId)); 61 | }, 62 | }); 63 | 64 | 65 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(UpdateEvent)); 66 | -------------------------------------------------------------------------------- /src/services/logging.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import OTKAnalytics from 'opentok-solutions-logging'; 3 | var pjson = require('../../package.json'); 4 | 5 | /** Analytics */ 6 | const logVariation = { 7 | attempt: 'Attempt', 8 | success: 'Success', 9 | fail: 'Fail', 10 | }; 11 | 12 | const logAction = { 13 | // vars for the analytics logs. Internal use 14 | init: 'Init', 15 | celebrityAcceptsCameraPermissions: 'CelebrityAcceptsCameraPermissions', 16 | celebrityConnects: 'CelebrityConnects', 17 | celebrityPublishes: 'CelebrityPublishes', 18 | celebritySubscribesToFan: 'CelebritySubscribesToFan', 19 | celebritySubscribesToHost: 'CelebritySubscribesToHost', 20 | fanConnectsOnstage: 'FanConnectsOnstage', 21 | fanConnectsBackstage: 'FanConnectsBackstage', 22 | fanPublishesBackstage: 'FanPublishesBackstage', 23 | hostConnects: 'HostConnects', 24 | hostAcceptsCameraPermissions: 'HostAcceptsCameraPermissions', 25 | hostPublishes: 'HostPublishes', 26 | hostSubscribesToFan: 'HostSubscribesToFan', 27 | hostSubscribesToCelebrity: 'HostSubscribesToCelebrity', 28 | producerConnects: 'ProducerConnects', 29 | producerMovesFanOnstage: 'ProducerMovesFanOnstage', 30 | producerGoLive: 'ProducerGoLive', 31 | producerEndShow: 'ProducerEndShow', 32 | }; 33 | 34 | class Analytics { 35 | 36 | constructor(source, sessionId, connectionId, apikey) { 37 | const otkanalyticsData = { 38 | clientVersion: 'js-ib-' + pjson.version, 39 | source, 40 | componentId: 'iBS', 41 | name: 'guidIB', 42 | partnerId: apikey, 43 | }; 44 | 45 | this.analytics = new OTKAnalytics(otkanalyticsData); 46 | 47 | if (connectionId) { 48 | this.update(sessionId, connectionId, apikey); 49 | } 50 | } 51 | 52 | update = (sessionId, connectionId, apiKey) => { 53 | if (sessionId && connectionId && apiKey) { 54 | const sessionInfo = { 55 | sessionId, 56 | connectionId, 57 | partnerId: apiKey, 58 | }; 59 | this.analytics.addSessionInfo(sessionInfo); 60 | } 61 | }; 62 | 63 | log = (action, variation) => { 64 | this.analytics.logEvent({ action, variation }); 65 | }; 66 | } 67 | 68 | module.exports = { 69 | Analytics, 70 | logVariation, 71 | logAction, 72 | }; 73 | -------------------------------------------------------------------------------- /src/components/Broadcast/CelebrityHost/components/CelebrityHostHeader.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import classNames from 'classnames'; 4 | import R from 'ramda'; 5 | import { isUserOnStage } from '../../../../services/util'; 6 | import './CelebrityHostHeader.css'; 7 | 8 | type Props = { 9 | userType: 'host' | 'celebrity', 10 | name: string, 11 | status: EventStatus, 12 | togglePublishOnly: boolean => void, 13 | publishOnlyEnabled: boolean, 14 | disconnected: boolean, 15 | privateCall: PrivateCallState, // eslint-disable-line react/no-unused-prop-types 16 | eventStarted: boolean 17 | }; 18 | const CelebrityHostHeader = (props: Props): ReactComponent => { 19 | const { userType, name, status, togglePublishOnly, publishOnlyEnabled, disconnected, eventStarted } = props; 20 | const btnClass = classNames('btn action', { red: !publishOnlyEnabled }, { green: publishOnlyEnabled }); 21 | const privateCallWith = R.path(['privateCall', 'isWith'], props); 22 | const inPrivateCall = R.equals(userType, privateCallWith); 23 | const otherInPrivateCall = !inPrivateCall && isUserOnStage(privateCallWith); 24 | return ( 25 |
    26 |
    27 |
    28 |

    {name}{status === 'notStarted' ? 'NOT STARTED' : status}

    29 | { status !== 'closed' && !disconnected && eventStarted && 30 |
    31 | 32 |
    33 | } 34 |
    35 |
    36 | {userType} 37 |
    38 |
    39 |
    40 | { inPrivateCall && 'You are in a private call with the Producer' } 41 | { otherInPrivateCall && `The ${privateCallWith} is in a private call with the producer and cannot currently hear you.` } 42 | { disconnected && 'Unable to establish connection, please check your network connection and refresh.' } 43 |
    44 |
    45 | ); 46 | }; 47 | 48 | export default CelebrityHostHeader; 49 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | For anyone looking to get involved to this project, we are glad to hear from you. Here are a few types of contributions 4 | that we would be interested in hearing about. 5 | 6 | * Bug fixes 7 | - If you find a bug, please first report it using Github Issues. 8 | - Issues that have already been identified as a bug will be labelled `bug`. 9 | - If you'd like to submit a fix for a bug, send a Pull Request from your own fork and mention the Issue number. 10 | * New Features 11 | - If you'd like to accomplish something in the library that it doesn't already do, describe the problem in a new 12 | Github Issue. 13 | - Issues that have been identified as a feature request will be labelled `enhancement`. 14 | - If you'd like to implement the new feature, please wait for feedback from the project maintainers before spending 15 | too much time writing the code. In some cases, `enhancement`s may not align well with the project objectives at 16 | the time. 17 | * Documentation and Miscellaneous 18 | - If you think the documentation could be clearer, you've got an alternative 19 | implementation of something that may have more advantages, or any other change we would still be glad hear about 20 | it. 21 | - If its a trivial change, go ahead and send a Pull Request with the changes you have in mind 22 | - If not, open a Github Issue to discuss the idea first. 23 | 24 | ## Requirements 25 | 26 | For a contribution to be accepted: 27 | 28 | * Code must follow existing styling conventions 29 | * Commit messages must be descriptive. Related issues should be mentioned by number. 30 | 31 | If the contribution doesn't meet these criteria, a maintainer will discuss it with you on the Issue. You can still 32 | continue to add more commits to the branch you have sent the Pull Request from. 33 | 34 | ## How To 35 | 36 | 1. Fork this repository on GitHub. 37 | 1. Clone/fetch your fork to your local development machine. 38 | 1. Create a new branch (e.g. `issue-12`, `feat.add_foo`, etc) and check it out. 39 | 1. Make your changes and commit them. 40 | 1. Push your new branch to your fork. (e.g. `git push myname issue-12`) 41 | 1. Open a Pull Request from your new branch to the original fork's `master` branch. -------------------------------------------------------------------------------- /src/components/Dashboard/components/DashboardHeader.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import { connect } from 'react-redux'; 4 | import { Link } from 'react-router'; 5 | import Icon from 'react-fontawesome'; 6 | import R from 'ramda'; 7 | import CopyToClipboard from '../../Common/CopyToClipboard'; 8 | import CopyEmbedCode from './CopyEmbedCode'; 9 | import './DashboardHeader.css'; 10 | 11 | /* beautify preserve:start */ 12 | type Props = { 13 | currentUser: User 14 | }; 15 | /* beautify preserve:end */ 16 | 17 | class DashboardHeader extends Component { 18 | 19 | props: Props; 20 | state: { 21 | embedMenuOpen: boolean 22 | } 23 | toggleEmbedMenu: Unit; 24 | 25 | constructor(props: Props) { 26 | super(props); 27 | this.state = { 28 | embedMenuOpen: false, 29 | }; 30 | this.toggleEmbedMenu = this.toggleEmbedMenu.bind(this); 31 | } 32 | 33 | toggleEmbedMenu() { 34 | const { embedMenuOpen } = this.state; 35 | this.setState({ embedMenuOpen: !embedMenuOpen }); 36 | } 37 | 38 | render(): ReactComponent { 39 | const { currentUser } = this.props; 40 | return ( 41 |
    42 |

    Dashboard

    43 |
    44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | { 52 | currentUser.superAdmin && 53 | 54 | 55 | 56 | } 57 | { 58 | !currentUser.superAdmin && 59 | 60 | 61 | 62 | } 63 |
    64 |
    65 | ); 66 | } 67 | } 68 | 69 | const mapStateToProps = (state: { currentUser: User }): Props => R.pick(['currentUser'], state); 70 | export default connect(mapStateToProps)(DashboardHeader); 71 | -------------------------------------------------------------------------------- /src/components/Broadcast/Producer/components/ProducerHeader.css: -------------------------------------------------------------------------------- 1 | .ProducerHeader { 2 | display: flex; 3 | padding: 10px 0px 10px 13px; 4 | background: #ffffff; 5 | height: 140px; 6 | flex-direction: column; 7 | } 8 | 9 | .ProducerHeader-info { 10 | display: flex; 11 | flex-direction: column; 12 | align-items: flex-start; 13 | justify-content: center; 14 | min-width: 700px; 15 | } 16 | 17 | .ProducerHeader-info a { 18 | color: #00a3e3; 19 | font-size: 13px; 20 | } 21 | 22 | .ProducerHeader-info h3 { 23 | color: #37363E; 24 | font-family: 'Montserrat', sans-serif; 25 | font-size: 22px; 26 | font-weight: 300; 27 | margin: 5px 0; 28 | } 29 | 30 | .ProducerHeader-info .post-production-url { 31 | display: flex; 32 | font-size: 10.5px; 33 | color: #58666E; 34 | align-items: center; 35 | margin-top: 5px; 36 | } 37 | 38 | .ProducerHeader-info .post-production-url .label { 39 | margin-right: 5px; 40 | } 41 | 42 | .ProducerHeader-info .post-production-url .btn { 43 | font-size: 12px; 44 | margin-left: 15px; 45 | height: 24px; 46 | } 47 | 48 | .ProducerHeader-controls { 49 | display: flex; 50 | flex-direction: row; 51 | height: 100%; 52 | align-items: flex-end; 53 | justify-content: flex-end; 54 | width: 100%; 55 | padding-right: 10px; 56 | } 57 | 58 | .ProducerHeader-controls .event-status { 59 | display: inline-block; 60 | font-size: 12px; 61 | padding: 1.5px; 62 | border: none; 63 | margin: 0 0 0 4px; 64 | border-radius: 3px; 65 | border-color: #26A65B; 66 | color: #26A65B; 67 | font-weight: 400; 68 | min-width: 90px; 69 | text-align: center; 70 | border: 2px solid; 71 | 72 | } 73 | 74 | .ProducerHeader-controls>button { 75 | height: 30px; 76 | margin: 0 2px; 77 | } 78 | 79 | .ProducerHeader-controls .btn.go-live { 80 | color: #30c06c; 81 | } 82 | 83 | .ProducerHeader-controls .btn.go-live .icon { 84 | margin-right: 5px; 85 | } 86 | 87 | .ProducerHeader-controls .btn.go-live:hover { 88 | background: #30c06c; 89 | color:#ffffff; 90 | } 91 | 92 | .ProducerHeader-controls .btn.end-show { 93 | min-width: 103px; 94 | } 95 | 96 | .ProducerHeader-controls .btn.white.copy-admin-id { 97 | min-width: 118px; 98 | } 99 | 100 | .ProducerHeader-controls .btn.end-show .icon { 101 | padding-right: 3px; 102 | } 103 | 104 | 105 | -------------------------------------------------------------------------------- /src/components/Broadcast/Fan/components/FanBody.css: -------------------------------------------------------------------------------- 1 | .FanBody { 2 | background-color: #000000; 3 | display: flex; 4 | justify-content: space-between; 5 | min-height: 50vh; 6 | border-radius: 0 0 3px 3px; 7 | border-bottom-left-radius: 4px; 8 | border-bottom-right-radius: 4px; 9 | } 10 | 11 | .FanBody .VideoWrap { 12 | flex: 1; 13 | align-items: center; 14 | border: 1px solid #000000; 15 | min-height: 50vh; 16 | } 17 | 18 | .FanBody .hide { 19 | display:none; 20 | } 21 | 22 | .FanBody .publisherWrapper { 23 | position: absolute; 24 | left: 1px; 25 | bottom: 0px; 26 | } 27 | 28 | .FanBody .publisherWrapper.hidePublisher { 29 | z-index:-1; 30 | } 31 | 32 | .FanBody .publisherWrapper .publisherActions { 33 | position: absolute; 34 | bottom: 160px; 35 | left: 0; 36 | width: 200px; 37 | height: 20px; 38 | background-color: #fff; 39 | } 40 | 41 | .FanBody .publisherWrapper .publisherActions button { 42 | display: inline-block; 43 | float: right; 44 | padding: 4px; 45 | color: #00a3e3; 46 | background: white; 47 | border: none; 48 | font-size: 13px; 49 | cursor: pointer; 50 | } 51 | 52 | .FanBody .publisherWrapper .publisherActions button:hover { 53 | color: #23527c; 54 | } 55 | 56 | .FanBody .publisherWrapper .publisherActions button:focus { 57 | outline: 0; 58 | } 59 | 60 | .FanBody .publisherWrapper.minimized .publisherActions { 61 | position: absolute; 62 | bottom: 10px; 63 | left: 9px; 64 | width: 20px; 65 | height: 20px; 66 | background-color: white; 67 | } 68 | 69 | .FanBody .publisherWrapper.minimized .publisherActions .restore { 70 | display: inline-block; 71 | float: right; 72 | padding: 4px; 73 | } 74 | 75 | .FanBody .VideoWrap.smallVideo { 76 | position: absolute; 77 | bottom: 0; 78 | left: 0; 79 | width: 200px; 80 | height: 160px; 81 | background: #444; 82 | z-index: 999; 83 | min-height: 0; 84 | } 85 | 86 | .FanBody .VideoWrap.smallVideo.hidePublisher { 87 | z-index: -1; 88 | } 89 | 90 | .FanBody .imageHolder { 91 | width: 100%; 92 | border: 1px solid #ddd; 93 | border-top: 0; 94 | border-bottom-left-radius: 4px; 95 | border-bottom-right-radius: 4px; 96 | background: #FFF; 97 | min-height: 200px; 98 | } 99 | 100 | .FanBody .imageHolder img { 101 | width: 100%; 102 | } 103 | 104 | .FanBody .producerContainer { 105 | position: absolute; 106 | z-index: -100; 107 | visibility: hidden; 108 | } 109 | -------------------------------------------------------------------------------- /src/components/Broadcast/Fan/components/FanHeader.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import classNames from 'classnames'; 4 | import R from 'ramda'; 5 | import { isUserOnStage } from '../../../../services/util'; 6 | import './FanHeader.css'; 7 | 8 | type Props = { 9 | name: string, 10 | status: EventStatus, 11 | ableToJoin: boolean, 12 | fanStatus: FanStatus, 13 | inPrivateCall: boolean, 14 | privateCall: PrivateCallState, 15 | getInLine: Unit, 16 | leaveLine: Unit, 17 | backstageConnected: boolean, 18 | disconnected: boolean, 19 | postProduction: boolean 20 | }; 21 | const FanHeader = (props: Props): ReactComponent => { 22 | const { 23 | name, 24 | status, 25 | ableToJoin, 26 | getInLine, 27 | leaveLine, 28 | backstageConnected, 29 | inPrivateCall, 30 | privateCall, 31 | fanStatus, 32 | disconnected, 33 | postProduction, 34 | } = props; 35 | const isConnecting = fanStatus === 'connecting'; 36 | const isDisconnecting = fanStatus === 'disconnecting'; 37 | const isOnStage = fanStatus === 'stage'; 38 | const displayGetInLineButton = ableToJoin && status !== 'closed' && !isOnStage && !isConnecting && !isDisconnecting && !postProduction && !disconnected; 39 | const inPrivateCallWith = R.propOr(null, 'isWith', privateCall || {}); 40 | const onStageUserInPrivateCall = !inPrivateCall && isOnStage && inPrivateCallWith && isUserOnStage(inPrivateCallWith); 41 | const getInLineButton = (): ReactComponent => 42 | !backstageConnected ? 43 | !isConnecting && : 44 | ; 45 | 46 | return ( 47 |
    48 |
    49 |

    {name}{status === 'notStarted' ? 'NOT STARTED' : status}

    50 | { displayGetInLineButton && 51 |
    52 | { getInLineButton() } 53 |
    54 | } 55 |
    56 |
    57 | { inPrivateCall && 'You are in a private call with the Producer' } 58 | { onStageUserInPrivateCall && `The ${inPrivateCallWith} is in a private call with the producer and cannot currently hear you.` } 59 | { disconnected && 'Unable to establish connection, please check your network connection and refresh.' } 60 |
    61 |
    62 | ); 63 | }; 64 | 65 | export default FanHeader; 66 | -------------------------------------------------------------------------------- /src/components/Dashboard/components/EventListHelpers.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import R from 'ramda'; 4 | import { Link } from 'react-router'; 5 | import moment from 'moment'; 6 | import Icon from 'react-fontawesome'; 7 | 8 | /** Event Name */ 9 | type NameProps = { id: string, status: EventStatus, name: string }; 10 | const eventName = ({ id, status, name }: NameProps): ReactComponent => // eslint-disable-line no-confusing-arrow 11 | status === 'closed' ? 12 | {name} : 13 | {name}; 14 | 15 | /** Event Timestamp */ 16 | type TimeProps = { id: string, status: EventStatus, dateTimeStart?: string, dateTimeEnd?: string, showStartedAt: string, showEndedAt: string }; 17 | const eventTime = ({ id, status, dateTimeStart = '', dateTimeEnd = '', showStartedAt = '', showEndedAt = '' }: TimeProps): ReactComponent => { 18 | 19 | const formattedDate = (d: string): string => moment(d).format('MMM DD, YYYY hh:mm A'); 20 | const formattedDuration = (start: string, end: string): string => moment.utc(moment(end).diff(moment(start))).format('HH:mm:ss'); 21 | 22 | const notEmpty = R.complement(R.isEmpty); 23 | const hasDates = R.and(notEmpty(dateTimeStart), notEmpty(dateTimeEnd)); 24 | const hasStartEndTimes = R.and(notEmpty(showStartedAt), notEmpty(showEndedAt)); 25 | 26 | const dateText = hasDates ? `${formattedDate(dateTimeStart)} to ${formattedDate(dateTimeEnd)}` : 'Date not provided'; 27 | const durationText = hasStartEndTimes && status === 'closed' ? formattedDuration(showStartedAt, showEndedAt) : null; 28 | 29 | const date = (): ReactComponent =>
    {dateText}
    ; 30 | const duration = (): ReactComponent => 31 |
    32 | Total time consumed: {durationText} 33 |
    ; 34 | 35 | return [date(), durationText && duration()]; 36 | }; 37 | 38 | /** Event Status (class name and text) */ 39 | const eventStatus = ({ status }: { status: EventStatus }): { style: string, text: string } => { 40 | switch (status) { 41 | case 'notStarted': 42 | return { style: 'created', text: 'Created' }; 43 | case 'live': 44 | return { style: 'live', text: 'Live' }; 45 | case 'closed': 46 | return { style: 'closed', text: 'Closed' }; 47 | case 'preshow': 48 | return { style: 'preshow', text: 'Preshow' }; 49 | default: 50 | return { style: '', text: '' }; 51 | } 52 | }; 53 | 54 | module.exports = { 55 | eventName, 56 | eventTime, 57 | eventStatus, 58 | }; 59 | -------------------------------------------------------------------------------- /src/components/Common/Chat.css: -------------------------------------------------------------------------------- 1 | .Chat { 2 | width: 260px; 3 | display: flex; 4 | flex-direction: column; 5 | border: 1px solid lightgrey; 6 | border-radius: 1px; 7 | } 8 | 9 | .Chat.hidden { 10 | visibility: hidden; 11 | width: 1px; 12 | height: 1px; 13 | } 14 | 15 | .ChatHeader { 16 | display: flex; 17 | justify-content: space-between; 18 | background-color: #00a3e3; 19 | height: 25px; 20 | } 21 | 22 | .ChatHeader .btn, 23 | .ChatHeader .icon { 24 | color: white; 25 | background-color: transparent; 26 | text-align: left; 27 | font-size: 13px; 28 | } 29 | 30 | .ChatHeader .btn.minimize:hover { 31 | text-decoration: underline; 32 | } 33 | 34 | .ChatActions { 35 | display: flex; 36 | justify-content: flex-start; 37 | background: #F5F5F5; 38 | padding: 5px; 39 | } 40 | 41 | .ChatActions .btn { 42 | margin-right: 3px; 43 | font-size: 12px; 44 | } 45 | 46 | .ChatPrivateCall { 47 | height: 125px; 48 | padding: 5px; 49 | background: black; 50 | display: none; 51 | } 52 | 53 | .ChatPrivateCall.inPrivateCall { 54 | display: initial; 55 | } 56 | 57 | .ChatMain { 58 | height: 250px; 59 | display: flex; 60 | flex-direction: column; 61 | justify-content: space-between; 62 | background: #E6E6E6; 63 | } 64 | 65 | .ChatMessages { 66 | overflow-y: scroll; 67 | padding: 5px; 68 | height: 258px; 69 | } 70 | 71 | .ChatMessages .Message { 72 | display: flex; 73 | width: 100%; 74 | justify-content: flex-start; 75 | margin-bottom: 6px; 76 | overflow-wrap: break-word; 77 | } 78 | 79 | .ChatMessages .Message.isMe { 80 | justify-content: flex-end; 81 | } 82 | 83 | .ChatMessages .Message .MessageText { 84 | max-width: 85%; 85 | font-family: 'Montserrat', sans-serif; 86 | font-weight: 300; 87 | text-align: left; 88 | padding: 3px 6px; 89 | background-color: white; 90 | border-radius: 8px; 91 | } 92 | 93 | .ChatMessages .Message.isMe .MessageText { 94 | background-color: lightblue; 95 | } 96 | 97 | .ChatForm { 98 | height: 40px; 99 | display: flex; 100 | justify-content: space-between; 101 | } 102 | 103 | .ChatForm * { 104 | border-radius: 0 !important; 105 | } 106 | 107 | .ChatForm input { 108 | padding: 2px; 109 | width: 85%; 110 | border: 1px darkgrey; 111 | } 112 | .ChatForm input:focus { 113 | outline:0; 114 | } 115 | 116 | .ChatForm .btn { 117 | width: 15%; 118 | } 119 | 120 | .ChatForm .btn.red { 121 | background-color: crimson; 122 | } -------------------------------------------------------------------------------- /src/components/Dashboard/components/CopyEmbedCode.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import { connect } from 'react-redux'; 4 | import R from 'ramda'; 5 | import Icon from 'react-fontawesome'; 6 | import onClickOutside from 'react-onclickoutside'; 7 | import classNames from 'classnames'; 8 | import CopyToClipboard from '../../Common/CopyToClipboard'; 9 | import createEmbed from '../../../services/createEmbed'; 10 | 11 | import './CopyEmbedCode.css'; 12 | 13 | type Props = { 14 | adminId: string 15 | }; 16 | 17 | class CopyEmbedCode extends Component { 18 | 19 | props: Props; 20 | state: { 21 | expanded: boolean 22 | } 23 | handleClickOutside: Unit; 24 | toggleExpanded: Unit; 25 | 26 | constructor(props: Props) { 27 | super(props); 28 | this.state = { 29 | expanded: false, 30 | }; 31 | this.toggleExpanded = this.toggleExpanded.bind(this); 32 | } 33 | 34 | handleClickOutside() { 35 | if (this.state.expanded) { 36 | this.toggleExpanded(); 37 | } 38 | } 39 | 40 | toggleExpanded() { 41 | this.setState({ expanded: !this.state.expanded }); 42 | } 43 | 44 | render(): ReactComponent { 45 | 46 | const { toggleExpanded } = this; 47 | const { expanded } = this.state; 48 | const fanCode = createEmbed('fan', this.props.adminId); 49 | const celebrityCode = createEmbed('celebrity', this.props.adminId); 50 | const hostCode = createEmbed('host', this.props.adminId); 51 | return ( 52 |
    53 | 56 |
    57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
    67 |
    68 | ); 69 | } 70 | } 71 | 72 | 73 | const mapStateToProps = (state: State): Props => ({ 74 | adminId: R.path(['currentUser', 'id'], state), 75 | }); 76 | 77 | export default connect(mapStateToProps)(onClickOutside(CopyEmbedCode)); 78 | -------------------------------------------------------------------------------- /src/components/Broadcast/Producer/components/ProducerPrimary.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import R from 'ramda'; 4 | import { connect } from 'react-redux'; 5 | import { withRouter } from 'react-router'; 6 | import classNames from 'classnames'; 7 | import Icon from 'react-fontawesome'; 8 | import Particpant from './Participant'; 9 | import { properCase } from '../../../../services/util'; 10 | import './ProducerPrimary.css'; 11 | 12 | const inPrivateCallWith = (privateCall: PrivateCallState, activeFans: ActiveFans): string => { 13 | const isWith = R.prop('isWith', privateCall); 14 | const fanId = R.prop('fanId', privateCall); 15 | const name = R.path(['map', fanId, 'name'], activeFans); 16 | if (R.equals('activeFan', isWith)) { 17 | return properCase(name); 18 | } else if (R.equals('fan', isWith)) { 19 | return `the Fan - ${name}`; 20 | } else if (R.equals('backstageFan', isWith)) { 21 | return `the Backstage Fan - ${name}`; 22 | } 23 | return `the ${properCase(isWith)}`; 24 | }; 25 | 26 | /* beautify preserve:start */ 27 | type Props = { 28 | broadcast: BroadcastState 29 | }; 30 | /* beautify preserve:end */ 31 | 32 | const ProducerPrimary = (props: Props): ReactComponent => { 33 | const { privateCall, viewers, interactiveLimit, activeFans, disconnected, elapsedTime } = props.broadcast; 34 | return ( 35 |
    36 |
    37 |
    38 | {interactiveLimit ? `Viewers ${viewers} / ${interactiveLimit}` : 'Retrieving viewers . . .'} 39 |
    40 |
    Elapsed time {elapsedTime}
    41 |
    42 | You are in a private call with { privateCall ? inPrivateCallWith(privateCall, activeFans) : '...' } 43 |
    44 |
    45 | Unable to establish connection, please check your network connection and refresh. 46 |
    47 |
    48 |
    49 | 50 | 51 | 52 | 53 |
    54 |
    55 |
    56 | ); 57 | }; 58 | 59 | 60 | const mapStateToProps = (state: State): Props => ({ 61 | broadcast: R.prop('broadcast', state), 62 | user: R.prop('currentUser', state), 63 | }); 64 | 65 | // Need withRouter??? 66 | export default withRouter(connect(mapStateToProps)(ProducerPrimary)); 67 | -------------------------------------------------------------------------------- /src/components/Login/components/LoginForm.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import R from 'ramda'; 4 | import Icon from 'react-fontawesome'; 5 | import classNames from 'classnames'; 6 | import './LoginForm.css'; 7 | 8 | /* beautify preserve:start */ 9 | type Props = { 10 | onSubmit: Unit, 11 | onUpdate: Unit, 12 | error: boolean, 13 | forgotPassword: boolean 14 | }; 15 | /* beautify preserve:end */ 16 | 17 | class LoginForm extends Component { 18 | 19 | props: Props; 20 | state: { 21 | fields: { 22 | email: string, 23 | password: string 24 | } 25 | } 26 | handleSubmit: Unit; 27 | handleChange: Unit; 28 | 29 | constructor(props: Props) { 30 | super(props); 31 | this.state = { 32 | fields: { 33 | email: '', 34 | password: '', 35 | }, 36 | }; 37 | this.handleSubmit = this.handleSubmit.bind(this); 38 | this.handleChange = this.handleChange.bind(this); 39 | } 40 | 41 | handleSubmit(e: SyntheticInputEvent) { 42 | e.preventDefault(); 43 | const { onSubmit } = this.props; 44 | const { email, password } = this.state.fields; 45 | onSubmit({ email, password }); 46 | } 47 | 48 | handleChange(e: SyntheticInputEvent) { 49 | const field = e.target.name; 50 | const value = e.target.value; 51 | const { onUpdate } = this.props; 52 | onUpdate(); 53 | this.setState({ fields: R.assoc(field, value, this.state.fields) }); 54 | } 55 | 56 | render(): ReactComponent { 57 | const { handleSubmit, handleChange } = this; 58 | const { error, forgotPassword } = this.props; 59 | const { email, password } = this.state.fields; 60 | const submitText = forgotPassword ? 'RESET PASSWORD' : 'SIGN IN'; 61 | return ( 62 |
    63 |
    64 | 65 | 66 |
    67 | { !forgotPassword && 68 |
    69 | 70 | 78 |
    79 | } 80 |
    81 | 82 |
    83 |
    84 | ); 85 | } 86 | } 87 | 88 | export default LoginForm; 89 | -------------------------------------------------------------------------------- /src/components/Broadcast/Producer/components/Participant.css: -------------------------------------------------------------------------------- 1 | .Participant { 2 | display: flex; 3 | flex-direction: column; 4 | width: 24%; 5 | height: 28%; 6 | } 7 | 8 | .Participant>div { 9 | width: 100%; 10 | position: relative; 11 | } 12 | 13 | .Participant-header { 14 | display: flex; 15 | justify-content: space-between; 16 | align-items: center; 17 | padding: 0 5px; 18 | font-size: 13px; 19 | color: #58666E; 20 | } 21 | 22 | .Participant-header .label { 23 | font-family: 'Montserrat', sans-serif; 24 | font-weight: 600; 25 | } 26 | 27 | .Participant-header .icon { 28 | margin-right: 5px; 29 | } 30 | 31 | .Participant-video { 32 | height: 200px; 33 | background-color: #e6e6e6; 34 | } 35 | 36 | .Participant-video .countdown-overlay { 37 | width: 100%; 38 | height: 100%; 39 | background-color: rgba(61, 61, 61, 0.51); 40 | display: flex; 41 | align-items: center; 42 | justify-content: center; 43 | } 44 | 45 | .Participant-video .countdown-overlay span { 46 | font-size: 75px; 47 | color: #FFFFFF; 48 | } 49 | 50 | .Participant .Participant-muted{ 51 | width: inherit; 52 | display: block; 53 | height: 20px; 54 | background-color: red; 55 | color: white; 56 | font-size: 20px; 57 | line-height: 20px; 58 | text-align: center; 59 | position: absolute; 60 | z-index: 1000; 61 | } 62 | 63 | .Participant-url, 64 | .Participant-move-fan, 65 | .Participant-feed-controls { 66 | margin-top: 5px; 67 | background-color: white; 68 | border: 1px solid #e5e5e5; 69 | border-radius: 3px; 70 | } 71 | 72 | .Participant-url, 73 | .Participant-move-fan, 74 | .Participant-feed-controls { 75 | display: flex; 76 | justify-content: space-between; 77 | align-items: center; 78 | height: 40px; 79 | padding: 0 5px; 80 | font-size: 12px; 81 | } 82 | 83 | .Participant-move-fan .move.btn { 84 | font-size: 12px; 85 | padding: 0; 86 | transition: color .75s; 87 | } 88 | 89 | .Participant-move-fan .move.btn:hover{ 90 | background: transparent; 91 | color: #23527c; 92 | } 93 | 94 | .Participant-url .url { 95 | width: 75%; 96 | overflow: hidden; 97 | white-space: nowrap; 98 | text-overflow: ellipsis; 99 | font-size: 12px; 100 | } 101 | 102 | .Participant-feed-controls .controls { 103 | display: flex; 104 | align-items: center; 105 | } 106 | 107 | .Participant-feed-controls .controls .control { 108 | width: 28px; 109 | height: 28px; 110 | margin-left: 2px; 111 | } 112 | 113 | .Participant-feed-controls .controls .control .icon { 114 | color: #939ca1; 115 | } 116 | 117 | .Participant-feed-controls .fa.icon.active { 118 | color: #58666E; 119 | } 120 | 121 | .fa.fa-circle.icon.green { 122 | color: #30c06c; 123 | } 124 | -------------------------------------------------------------------------------- /src/actions/auth.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import R from 'ramda'; 3 | import firebase from '../services/firebase'; 4 | import { getAuthToken, getAuthTokenUser } from '../services/api'; 5 | import { saveAuthToken, saveState } from '../services/localStorage'; 6 | import { logIn, logOut } from './currentUser'; 7 | import { setSuccess, resetAlert, setInfo } from './alert'; 8 | 9 | const authError: ActionCreator = (error: null | Error): AuthAction => ({ 10 | type: 'AUTH_ERROR', 11 | error, 12 | }); 13 | 14 | const userForgotPassword: ThunkActionCreator = (forgot: boolean): Thunk => 15 | (dispatch: Dispatch) => { 16 | dispatch(authError(null)); 17 | dispatch({ type: 'AUTH_FORGOT_PASSWORD', forgot }); 18 | }; 19 | 20 | const validate: ThunkActionCreator = (uid: string, idToken: string): Thunk => 21 | async (dispatch: Dispatch): AsyncVoid => { 22 | try { 23 | const { token } = await getAuthToken(idToken); 24 | saveAuthToken(token); 25 | dispatch(logIn(uid)); 26 | } catch (error) { 27 | await dispatch(authError(error)); 28 | } 29 | }; 30 | 31 | const validateUser: ThunkActionCreator = (adminId: string, userType: UserRole, userUrl: string): Thunk => 32 | async (dispatch: Dispatch): AsyncVoid => { 33 | try { 34 | const { token } = await getAuthTokenUser(adminId, userType, userUrl); 35 | dispatch({ type: 'SET_AUTH_TOKEN', token }); 36 | } catch (error) { 37 | await dispatch(authError(error)); 38 | } 39 | }; 40 | 41 | const signIn: ThunkActionCreator = ({ email, password }: AuthCredentials): Thunk => 42 | async (dispatch: Dispatch): AsyncVoid => { 43 | try { 44 | const user = await firebase.auth().signInWithEmailAndPassword(email, password); 45 | const idToken = await user.getIdToken(true); 46 | await dispatch(validate(R.prop('uid', user), idToken)); 47 | } catch (error) { 48 | await dispatch(authError(error)); 49 | } 50 | }; 51 | 52 | const signOut: ThunkActionCreator = (): Thunk => 53 | (dispatch: Dispatch) => { 54 | dispatch(logOut()); 55 | // We need to ensure the localstorage is updated ASAP 56 | saveState({ currentUser: null }); 57 | firebase.auth().signOut().then((): void => (window.location.href = '/')); 58 | }; 59 | 60 | const resetPassword: ThunkActionCreator = ({ email }: AuthCredentials): Thunk => 61 | async (dispatch: Dispatch): AsyncVoid => { 62 | try { 63 | await firebase.auth().sendPasswordResetEmail(email); 64 | const options: AlertPartialOptions = { 65 | title: 'Password Reset', 66 | text: 'Please check your inbox for password reset instructions', 67 | onConfirm: (): void => R.forEach(dispatch, [resetAlert(), userForgotPassword(false)]), 68 | }; 69 | dispatch(setSuccess(options)); 70 | } catch (error) { 71 | dispatch(setInfo({ title: 'Password Reset', text: 'We couldn\'t find an account for that email address.' })); 72 | } 73 | }; 74 | 75 | module.exports = { 76 | signIn, 77 | signOut, 78 | userForgotPassword, 79 | resetPassword, 80 | validateUser, 81 | }; 82 | -------------------------------------------------------------------------------- /src/components/Broadcast/Fan/components/FanBody.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import classNames from 'classnames'; 4 | import R from 'ramda'; 5 | import VideoHolder from '../../../Common/VideoHolder'; 6 | import FanHLSPlayer from './FanHLSPlayer'; 7 | import './FanBody.css'; 8 | import defaultImg from '../../../../images/TAB_VIDEO_PREVIEW_LS.jpg'; 9 | 10 | const userTypes: ParticipantType[] = ['host', 'celebrity', 'fan']; 11 | 12 | type Props = { 13 | isClosed: boolean, 14 | isLive: boolean, 15 | image?: EventImage, 16 | participants: BroadcastParticipants, 17 | hasStreams: boolean, 18 | backstageConnected: boolean, 19 | fanStatus: FanStatus, 20 | ableToJoin: boolean, 21 | hlsUrl: string, 22 | postProduction: boolean, 23 | publisherMinimized: boolean, 24 | restorePublisher: Unit, 25 | minimizePublisher: Unit 26 | }; 27 | 28 | const FanBody = (props: Props): ReactComponent => { 29 | const { 30 | isClosed, 31 | isLive, 32 | image, 33 | participants, 34 | hasStreams, 35 | backstageConnected, 36 | fanStatus, 37 | ableToJoin, 38 | hlsUrl, 39 | postProduction, 40 | publisherMinimized, 41 | minimizePublisher, 42 | restorePublisher, 43 | } = props; 44 | const fanOnStage = R.equals('stage', fanStatus); 45 | const showImage = ((!isLive && !postProduction) || (!hasStreams && ableToJoin)) && !fanOnStage; 46 | const hidePublisher = isClosed || !backstageConnected || fanOnStage; 47 | const shouldSubscribe = isLive || fanOnStage || postProduction; 48 | const showHLSPlayer = isLive && !ableToJoin && hlsUrl; 49 | const isInLine = fanStatus !== 'disconnected' && fanStatus !== 'connected'; 50 | const mainClassNames = classNames('FanBody', { inLine: isInLine }); 51 | return ( 52 |
    53 | { showImage && 54 |
    55 | event 56 |
    57 | } 58 | { !isClosed && 59 | userTypes.map((type: ParticipantType): ReactComponent => 60 | ) 65 | } 66 | { showHLSPlayer && } 67 |
    68 |
    69 | { !publisherMinimized && } 70 | { publisherMinimized && } 71 |
    72 |
    73 |
    74 |
    75 |
    76 | ); 77 | }; 78 | 79 | export default FanBody; 80 | -------------------------------------------------------------------------------- /src/components/Broadcast/Producer/Producer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import R from 'ramda'; 4 | import { connect } from 'react-redux'; 5 | import { withRouter } from 'react-router'; 6 | import classNames from 'classnames'; 7 | import ProducerHeader from './components/ProducerHeader'; 8 | import ProducerSidePanel from './components/ProducerSidePanel'; 9 | import ProducerPrimary from './components/ProducerPrimary'; 10 | import NetworkReconnect from '../../Common/NetworkReconnect'; 11 | import ProducerChat from './components/ProducerChat'; 12 | import { initializeBroadcast, resetBroadcastEvent } from '../../../actions/producer'; 13 | import './Producer.css'; 14 | 15 | /* beautify preserve:start */ 16 | type InitialProps = { params: { id?: string } }; 17 | type BaseProps = { 18 | user: User, 19 | eventId: EventId, 20 | broadcast: BroadcastState 21 | }; 22 | type DispatchProps = { 23 | setEvent: EventId => void, 24 | resetEvent: Unit 25 | }; 26 | 27 | type Props = InitialProps & BaseProps & DispatchProps; 28 | /* beautify preserve:end */ 29 | 30 | class Producer extends Component { 31 | props: Props; 32 | state: { preshowStarted: boolean, showingSidePanel: boolean }; 33 | startPreshow: Unit; 34 | toggleSidePanel: Unit; 35 | signalListener: Signal => void; 36 | constructor(props: Props) { 37 | super(props); 38 | this.state = { 39 | preshowStarted: false, 40 | showingSidePanel: true, 41 | }; 42 | this.toggleSidePanel = this.toggleSidePanel.bind(this); 43 | } 44 | 45 | toggleSidePanel() { 46 | this.setState({ showingSidePanel: !this.state.showingSidePanel }); 47 | } 48 | 49 | componentDidMount() { 50 | const { setEvent, eventId } = this.props; 51 | setEvent(eventId); 52 | } 53 | 54 | componentWillUnmount() { 55 | this.props.resetEvent(); 56 | } 57 | 58 | render(): ReactComponent { 59 | const { toggleSidePanel } = this; 60 | const { showingSidePanel } = this.state; 61 | const { broadcast } = this.props; 62 | return ( 63 |
    64 | 65 |
    66 | 67 | 68 |
    69 |
    72 | ); 73 | } 74 | } 75 | 76 | const mapStateToProps = (state: State, ownProps: InitialProps): BaseProps => ({ 77 | eventId: R.pathOr('', ['params', 'id'], ownProps), 78 | broadcast: R.prop('broadcast', state), 79 | user: R.prop('currentUser', state), 80 | }); 81 | 82 | const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch): DispatchProps => 83 | ({ 84 | setEvent: (eventId: EventId) => { 85 | dispatch(initializeBroadcast(eventId, 'producer')); 86 | }, 87 | resetEvent: () => { 88 | dispatch(resetBroadcastEvent()); 89 | }, 90 | }); 91 | 92 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Producer)); 93 | -------------------------------------------------------------------------------- /flowtypes/events.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* eslint no-undef: "off" */ 3 | /* beautify preserve:start */ 4 | 5 | declare type EventImage = { 6 | url: string, 7 | id: string 8 | } 9 | 10 | declare type BroadcastEvent = { 11 | id: string, 12 | name: string, 13 | status: EventStatus, 14 | archiveEvent: boolean, 15 | fanUrl: string, 16 | hostUrl: string, 17 | showEnded: string, 18 | showStarted: string, 19 | startImage?: EventImage, 20 | endImage?: EventImage, 21 | fanUrl: string, 22 | celebrityUrl: string, 23 | hostUrl: string, 24 | archiveEvent: boolean, 25 | dateTimeStart?: string, 26 | dateTimeEnd?: string, 27 | sessionId: string, 28 | stageSessionId: string, 29 | archiveUrl?: string, 30 | rtmpUrl?: string, 31 | redirectUrl: string, 32 | uncomposed: boolean, 33 | showStartedAt: string, 34 | showEndedAt: string, 35 | adminId: string, 36 | createdAt: string, 37 | updatedAt: string 38 | } 39 | 40 | declare type ActiveBroadcast = { 41 | hlsUrl?: string, 42 | name?: string, 43 | status: EventStatus, 44 | startImage?: EventImage, 45 | endImage?: EventImage, 46 | archiveUrl?: string 47 | } 48 | 49 | declare type BroadcastEventMap = {[id: EventId]: BroadcastEvent}; 50 | 51 | declare type EventStatus = 'notStarted' | 'preshow' | 'live' | 'closed'; 52 | declare type EventFilter = 'all' | 'current' | 'archived'; 53 | declare type EventSortByOption = 'mostRecent' | 'startDate' | 'state'; 54 | declare type EventOrderOption = 'ascending' | 'descending'; 55 | declare type EventSorting = { sortBy: EventSortByOption, order: EventOrderOption } 56 | declare type EventId = string; 57 | declare type EventsState = { 58 | map: null | BroadcastEventMap, 59 | mostRecent: null | BroadcastEvent, 60 | filter: EventFilter, 61 | sorting: EventSorting, 62 | active: null | EventId 63 | }; 64 | declare type EventsAction = 65 | { type: 'SET_EVENTS', events: BroadcastEventMap } | 66 | { type: 'SET_MOST_RECENT_EVENT', event: BroadcastEvent } | 67 | { type: 'UPDATE_EVENT', event: BroadcastEvent } | 68 | { type: 'SUBMIT_FORM_EVENT', event: BroadcastEvent, submitting: boolean } | 69 | { type: 'FILTER_EVENTS', filter: EventFilter } | 70 | { type: 'SORT_EVENTS', sortBy: EventSortByOption } | 71 | { type: 'DELETE_BROADCAST_PROMPT', id: string, onConfirm: string => void } | 72 | { type: 'CREATE_EVENT', event: BroadcastEvent } | 73 | { type: 'REMOVE_EVENT', id: string }; 74 | 75 | 76 | declare type BroadcastEventFormData = { 77 | name: string, 78 | adminId?: string, 79 | startImage?: EventImage, 80 | endImage?: EventImage, 81 | dateTimeStart?: string, 82 | dateTimeEnd?: string, 83 | archiveEvent: boolean, 84 | fanUrl: string, 85 | fanAudioUrl: string, 86 | hostUrl: string, 87 | celebrityUrl: string, 88 | redirectUrl?: string, 89 | uncomposed: boolean, 90 | submitting: boolean 91 | } 92 | 93 | declare type BroadcastEventUpdateFormData = BroadcastEventFormData & { id: string } 94 | 95 | declare type EventUrls = { 96 | fanUrl: string, 97 | fanAudioUrl: string, 98 | hostUrl: string, 99 | celebrityUrl: string 100 | }; 101 | -------------------------------------------------------------------------------- /src/components/Users/components/UserList.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import { connect } from 'react-redux'; 4 | import { withRouter } from 'react-router'; 5 | import R from 'ramda'; 6 | import UserActions from './UserActions'; 7 | import EditUser from './EditUser'; 8 | import AddUser from './AddUser'; 9 | import { deleteUser } from '../../../actions/users'; 10 | import './UserList.css'; 11 | 12 | type ListItemProps = { user: User, adminId: string }; 13 | class UserListItem extends Component { 14 | props: ListItemProps; 15 | state: { editingUser: false }; 16 | toggleEditPanel: Unit; 17 | 18 | constructor(props: ListItemProps) { 19 | super(props); 20 | this.state = { editingUser: false }; 21 | this.toggleEditPanel = this.toggleEditPanel.bind(this); 22 | } 23 | 24 | toggleEditPanel() { 25 | this.setState({ editingUser: !this.state.editingUser }); 26 | } 27 | 28 | render(): ReactComponent { 29 | const { user, adminId } = this.props; 30 | const { editingUser } = this.state; 31 | const { toggleEditPanel } = this; 32 | const hasAPIKey = (user && user.otApiKey); 33 | return ( 34 |
    35 | { !hasAPIKey && adminId &&
    36 | Please complete your APIKey and Secret 37 |
    38 | } 39 |
    40 | { !editingUser && 41 |
    42 | {user.displayName} 43 | [ {user.email} ] 44 |
    45 | } 46 | { editingUser ? : } 47 |
    48 |
    49 | ); 50 | } 51 | } 52 | 53 | type BaseProps = { users: User[], currentUser: User }; 54 | type DispatchProps = { delete: UserId => void }; 55 | type InitialProps = { adminId: string }; 56 | type Props = BaseProps & DispatchProps & InitialProps; 57 | const renderUser = (user: User, adminId: string): ReactComponent => 58 | ; 59 | const UserList = ({ users, adminId, currentUser }: Props): ReactComponent => 60 |
      61 | { 62 | !adminId && R.ifElse( 63 | R.isEmpty, 64 | (): null => null, 65 | R.map(renderUser) // eslint-disable-line comma-dangle 66 | )(R.values(users)) 67 | } 68 | { adminId && renderUser(currentUser, adminId) } 69 | { !adminId && } 70 |
    ; 71 | 72 | 73 | const mapStateToProps = (state: State, ownProps: InitialProps): BaseProps => ({ 74 | users: R.path(['users'], state), 75 | adminId: R.path(['params', 'adminId'], ownProps), 76 | currentUser: R.path(['currentUser'], state), 77 | }); 78 | 79 | const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch): DispatchProps => 80 | ({ 81 | delete: (userId: UserId) => { 82 | dispatch(deleteUser(userId)); 83 | }, 84 | }); 85 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(UserList)); 86 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import url('./css/react-redux-toastr.css'); 2 | * { 3 | box-sizing: border-box; 4 | font-family: 'Open Sans', sans-serif; 5 | } 6 | 7 | body { 8 | margin: 0; 9 | padding: 0; 10 | color: #58666E; 11 | } 12 | 13 | a, 14 | a:visited, 15 | a:hover, 16 | a:active { 17 | text-decoration: none; 18 | color: #00a3e3; 19 | } 20 | 21 | input.error { 22 | border-color: red !important; 23 | } 24 | 25 | .btn { 26 | display: flex; 27 | justify-content: center; 28 | align-items: center; 29 | font-size: 14px; 30 | color: #fff; 31 | background-color: #00a3e3; 32 | border: none; 33 | border-radius: 3px; 34 | cursor: pointer; 35 | } 36 | 37 | .btn:active, 38 | .btn:focus, 39 | .btn.active { 40 | background-image: none; 41 | outline: 0; 42 | box-shadow: none; 43 | } 44 | 45 | .btn.inactive { 46 | cursor: initial; 47 | } 48 | 49 | .btn.disabled { 50 | cursor: not-allowed; 51 | } 52 | 53 | .btn.white { 54 | color: rgb(88, 102, 110); 55 | background-color: white; 56 | border: 1px solid rgb(232, 235, 237); 57 | } 58 | 59 | .btn.white:hover { 60 | background-color: rgb(249, 249, 249); 61 | } 62 | 63 | .btn.red { 64 | color: #ffffff; 65 | background-color: #EF4836; 66 | border: 1px solid rgb(232, 235, 237); 67 | } 68 | 69 | .btn.red:hover { 70 | background-color: #f15949; 71 | } 72 | 73 | .btn.green { 74 | color: #ffffff; 75 | background-color: #26A65B; 76 | border: 1px solid rgb(232, 235, 237); 77 | } 78 | 79 | .btn.green:hover { 80 | background-color: #139348; 81 | } 82 | 83 | .btn.green.getInLine, 84 | .btn.red.getInLine { 85 | margin-top: 10px; 86 | } 87 | 88 | .btn.transparent { 89 | color: #00a3e3; 90 | background-color: transparent; 91 | border: none; 92 | } 93 | 94 | .btn.transparent:hover { 95 | background-color: #eee; 96 | } 97 | 98 | .btn.action { 99 | height: 34px; 100 | margin: 0 5px; 101 | padding: 7px 20px; 102 | } 103 | 104 | .btn.action.green { 105 | background-color: #30c06c; 106 | } 107 | 108 | .btn.action.blue { 109 | background-color: #46b8da; 110 | } 111 | 112 | .btn.action.orange { 113 | background-color: rgba(243, 156,18,.80); 114 | } 115 | 116 | .btn.action.red { 117 | background-color: #EF4836; 118 | } 119 | 120 | .btn.action.grey { 121 | color: rgb(88, 102, 110); 122 | background-color: #E4E4E4; 123 | } 124 | 125 | .btn.action span.fa { 126 | margin-right: 5px; 127 | } 128 | 129 | .link.white, 130 | .link.white:visited, 131 | .link.white:hover, 132 | .link.white:active { 133 | color: white; 134 | } 135 | 136 | .admin-page-header { 137 | height: 92px; 138 | } 139 | 140 | .admin-page-content { 141 | background: #F5F5F5; 142 | padding: 20px; 143 | height: 100vh; 144 | } 145 | 146 | .admin-page-list { 147 | background-color: white; 148 | margin: 10px 5px; 149 | padding: 10px; 150 | list-style-type: none; 151 | } 152 | 153 | .admin-page-list-item { 154 | display: flex; 155 | justify-content: space-between; 156 | margin-bottom: 10px; 157 | padding: 10px; 158 | border: 1px solid #e8ebed; 159 | border-radius: 3px; 160 | } 161 | -------------------------------------------------------------------------------- /src/actions/alert.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import R from 'ramda'; 3 | import { browserHistory } from 'react-router'; 4 | 5 | const setAlert: ActionCreator = (options: AlertState): AlertAction => ({ 6 | type: 'SET_ALERT', 7 | options, 8 | }); 9 | 10 | const resetAlert: ActionCreator = (): AlertAction => ({ 11 | type: 'RESET_ALERT', 12 | }); 13 | 14 | const setWarning: ThunkActionCreator = (options: AlertPartialOptions): Thunk => 15 | (dispatch: Dispatch) => { 16 | const defaultOptions = { 17 | show: true, 18 | type: 'warning', 19 | title: 'Warning', 20 | text: '', 21 | onConfirm: (): void => dispatch(resetAlert()), 22 | showCancelButton: true, 23 | }; 24 | dispatch(setAlert(R.merge(defaultOptions, options))); 25 | }; 26 | const setSuccess: ThunkActionCreator = (options: AlertPartialOptions): Thunk => 27 | (dispatch: Dispatch) => { 28 | const defaultOptions = { 29 | show: true, 30 | type: 'success', 31 | title: 'Success', 32 | text: null, 33 | onConfirm: (): void => dispatch(resetAlert()), 34 | }; 35 | dispatch(setAlert(R.merge(defaultOptions, options))); 36 | }; 37 | 38 | const setError: ThunkActionCreator = (text: string): Thunk => 39 | (dispatch: Dispatch) => { 40 | const options = { 41 | show: true, 42 | type: 'error', 43 | title: 'Error', 44 | text, 45 | html: true, 46 | onConfirm: (): void => dispatch(resetAlert()), 47 | }; 48 | dispatch(setAlert(options)); 49 | }; 50 | 51 | const setInfo: ThunkActionCreator = (options: AlertPartialOptions): Thunk => 52 | (dispatch: Dispatch) => { 53 | const defaultOptions = { 54 | show: true, 55 | type: 'info', 56 | title: null, 57 | text: null, 58 | onConfirm: (): void => dispatch(resetAlert()), 59 | }; 60 | dispatch(setAlert(R.merge(defaultOptions, options))); 61 | }; 62 | 63 | const setBlockUserAlert: ThunkActionCreator = (): Thunk => 64 | (dispatch: Dispatch) => { 65 | const options = { 66 | show: true, 67 | title: '', 68 | text: 'It seems you have the event opened in another tab. Please make sure you have only one tab opened at a time.', 69 | showConfirmButton: true, 70 | html: true, 71 | allowEscapeKey: false, 72 | onConfirm: () => { 73 | browserHistory.push('/admin'); 74 | dispatch(resetAlert()); 75 | }, 76 | }; 77 | dispatch(setAlert(options)); 78 | }; 79 | 80 | const setCameraError: ThunkActionCreator = (): Thunk => 81 | (dispatch: Dispatch) => { 82 | const text = 'Please allow access to your camera and microphone to continue.' + 83 | '
    Click the camera icon in your browser bar to view the permissions dialog.'; 84 | const options = { 85 | show: true, 86 | title: 'Aw, what happened?', 87 | type: 'error', 88 | text, 89 | showConfirmButton: true, 90 | html: true, 91 | onConfirm: (): void => dispatch(resetAlert()), 92 | }; 93 | dispatch(setAlert(options)); 94 | }; 95 | 96 | module.exports = { 97 | setAlert, 98 | setError, 99 | setInfo, 100 | setSuccess, 101 | setWarning, 102 | resetAlert, 103 | setBlockUserAlert, 104 | setCameraError, 105 | }; 106 | -------------------------------------------------------------------------------- /src/components/Login/Login.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import { connect } from 'react-redux'; 4 | import { browserHistory } from 'react-router'; 5 | import R from 'ramda'; 6 | import classNames from 'classnames'; 7 | import { userForgotPassword, resetPassword, signIn } from '../../actions/auth'; 8 | import LoginForm from './components/LoginForm'; 9 | import logo from '../../images/logo.png'; 10 | import './Login.css'; 11 | 12 | /* beautify preserve:start */ 13 | type BaseProps = { auth: AuthState, currentUser: User }; 14 | type DispatchProps = { 15 | authenticateUser: (credentials: AuthCredentials) => void, 16 | onForgotPassword: boolean => void, 17 | sendResetEmail: (email: AuthCredentials) => void 18 | }; 19 | type Props = BaseProps & DispatchProps; 20 | /* beautify preserve:end */ 21 | 22 | class Login extends Component { 23 | 24 | props: Props; 25 | 26 | state: { 27 | error: boolean 28 | }; 29 | 30 | resetError: Unit; 31 | handleSubmit: Unit; 32 | 33 | constructor(props: Props) { 34 | super(props); 35 | this.state = { error: false }; 36 | this.resetError = this.resetError.bind(this); 37 | this.handleSubmit = this.handleSubmit.bind(this); 38 | } 39 | 40 | componentDidMount() { 41 | const { currentUser } = this.props; 42 | if (currentUser) { 43 | browserHistory.push('/admin'); 44 | } 45 | } 46 | 47 | handleSubmit(credentials: AuthCredentials) { 48 | const { sendResetEmail, authenticateUser } = this.props; 49 | this.props.auth.forgotPassword ? sendResetEmail(credentials) : authenticateUser(credentials); 50 | } 51 | 52 | componentWillReceiveProps(nextProps: Props) { 53 | const error = (R.path(['auth', 'error'], nextProps)); 54 | this.setState({ error }); 55 | } 56 | 57 | resetError() { 58 | this.setState({ error: false }); 59 | } 60 | 61 | render(): ReactComponent { 62 | const { resetError, handleSubmit } = this; 63 | const { error } = this.state; 64 | const { onForgotPassword } = this.props; 65 | const { forgotPassword } = this.props.auth; 66 | return ( 67 |
    68 |
    69 | opentok 70 |
    71 | 72 |
    73 | { error &&
    Please check your credentials and try again
    } 74 | 77 |
    78 |
    79 | ); 80 | } 81 | } 82 | 83 | const mapStateToProps = (state: State): BaseProps => R.pick(['auth', 'currentUser'], state); 84 | const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch): DispatchProps => 85 | ({ 86 | authenticateUser: (credentials: AuthCredentials) => { 87 | dispatch(signIn(credentials)); 88 | }, 89 | onForgotPassword: (forgot: boolean) => { 90 | dispatch(userForgotPassword(forgot)); 91 | }, 92 | sendResetEmail: (credentials: AuthCredentials) => { 93 | dispatch(resetPassword(credentials)); 94 | }, 95 | }); 96 | export default connect(mapStateToProps, mapDispatchToProps)(Login); 97 | -------------------------------------------------------------------------------- /src/actions/users.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import R from 'ramda'; 3 | import { resetAlert, setError, setWarning, setSuccess } from './alert'; 4 | import firebase from '../services/firebase'; 5 | import { getAllUsers, deleteUserRecord, updateUser as update, createUser } from '../services/api'; 6 | import { setCurrentUser } from './currentUser'; 7 | 8 | 9 | const setUsers: ActionCreator = (users: UserMap): ManageUsersAction => ({ 10 | type: 'SET_USERS', 11 | users, 12 | }); 13 | 14 | const updateUser: ActionCreator = (user: User): ManageUsersAction => ({ 15 | type: 'UPDATE_USER', 16 | user, 17 | }); 18 | 19 | const removeUser: ActionCreator = (userId: UserId): ManageUsersAction => ({ 20 | type: 'REMOVE_USER', 21 | userId, 22 | }); 23 | 24 | const getUsers: ThunkActionCreator = (): Thunk => 25 | async (dispatch: Dispatch): AsyncVoid => { 26 | try { 27 | const users = await getAllUsers(); 28 | dispatch(setUsers(users)); 29 | } catch (error) { 30 | console.log(error); 31 | } 32 | }; 33 | 34 | const confirmDeleteUser: ThunkActionCreator = (userId: UserId): Thunk => 35 | async (dispatch: Dispatch): AsyncVoid => { 36 | try { 37 | await deleteUserRecord(userId); 38 | dispatch(removeUser(userId)); 39 | } catch (error) { 40 | console.log(error); 41 | } 42 | }; 43 | 44 | const deleteUser: ThunkActionCreator = (userId: UserId): Thunk => 45 | (dispatch: Dispatch) => { 46 | const options: AlertPartialOptions = { 47 | title: 'Delete User', 48 | text: 'Are you sure you wish to delete this user? All events associated with the user will also be deleted.', 49 | showCancelButton: true, 50 | onConfirm: (): void => R.forEach(dispatch, [resetAlert(), confirmDeleteUser(userId)]), 51 | onCancel: (): void => dispatch(resetAlert()), 52 | }; 53 | dispatch(setWarning(options)); 54 | }; 55 | 56 | const updateUserRecord: ThunkActionCreator = (userData: UserUpdateFormData): Thunk => 57 | async (dispatch: Dispatch, getState: GetState): AsyncVoid => { 58 | try { 59 | await update(userData); 60 | const currentUser = R.path(['currentUser'], getState()); 61 | const options: AlertPartialOptions = { 62 | title: 'User Updated', 63 | text: `The user record for ${userData.displayName} has been updated.`, 64 | onConfirm: (): void => dispatch(resetAlert()) && dispatch(updateUser(userData)), 65 | }; 66 | if (currentUser.id === userData.id) { 67 | dispatch(setCurrentUser(R.merge(currentUser, userData))); 68 | } 69 | dispatch(setSuccess(options)); 70 | } catch (error) { 71 | dispatch(setError('Failed to update user. Please check credentials and try again.')); 72 | } 73 | }; 74 | 75 | const createNewUser: ThunkActionCreator = (user: UserFormData): Thunk => 76 | async (dispatch: Dispatch): AsyncVoid => { 77 | try { 78 | const newUser = await createUser(user); 79 | await firebase.auth().sendPasswordResetEmail(newUser.email); 80 | const options: AlertPartialOptions = { 81 | title: 'User Created', 82 | text: `${newUser.displayName} has been created as a new user.`, 83 | onConfirm: (): void => R.forEach(dispatch, [resetAlert(), updateUser(newUser)]), 84 | }; 85 | dispatch(setSuccess(options)); 86 | } catch (error) { 87 | dispatch(setError('Failed to create user. Please check credentials and try again.')); 88 | } 89 | }; 90 | 91 | module.exports = { 92 | getUsers, 93 | setUsers, 94 | deleteUser, 95 | createNewUser, 96 | updateUserRecord, 97 | }; 98 | -------------------------------------------------------------------------------- /src/components/Broadcast/Producer/components/ProducerHeader.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import R from 'ramda'; 4 | import { Link } from 'react-router'; 5 | import { connect } from 'react-redux'; 6 | import truncate from 'lodash.truncate'; 7 | import Icon from 'react-fontawesome'; 8 | import CopyToClipboard from '../../../Common/CopyToClipboard'; 9 | import createUrls from '../../../../services/eventUrls'; 10 | import { changeStatus, goLive } from '../../../../actions/producer'; 11 | import './ProducerHeader.css'; 12 | 13 | type InitialProps = { 14 | showingSidePanel: boolean, 15 | toggleSidePanel: Unit 16 | }; 17 | 18 | type BaseProps = { 19 | broadcast: BroadcastState, 20 | currentUser: User 21 | }; 22 | 23 | type DispatchProps = { 24 | goLive: string => void, 25 | endShow: string => void 26 | }; 27 | 28 | type Props = InitialProps & BaseProps & DispatchProps; 29 | 30 | const ProducerHeader = ({ broadcast, showingSidePanel, toggleSidePanel, currentUser, goLive, endShow }: Props): ReactComponent => { 31 | const event = R.defaultTo({})(broadcast.event); 32 | const { status, archiveId } = event; 33 | const { connected, archiving, disconnected } = broadcast; 34 | const { fanAudioUrl } = createUrls(event); 35 | 36 | return ( 37 |
    38 |
    39 | Back to Events 40 |

    { event.name }

    41 |
    42 | POST-PRODUCTION URL: 43 | {truncate(fanAudioUrl, { length: 85 })} 44 | 45 | 46 | 47 |
    48 |
    49 |
    50 | { status === 'live' && (archiving || archiveId) && 51 | 52 | ARCHIVING 53 | 54 | } 55 | { status === 'preshow' && !disconnected && 56 | 60 | } 61 | { status === 'live' && !disconnected && 62 | 66 | } 67 | 68 | 69 | 70 | 73 |
    74 |
    ); 75 | }; 76 | const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch): DispatchProps => 77 | ({ 78 | goLive: (id: string) => { 79 | dispatch(goLive(id)); 80 | }, 81 | endShow: (id: string) => { 82 | dispatch(changeStatus(id, 'closed')); 83 | }, 84 | }); 85 | const mapStateToProps = (state: State): BaseProps => R.pick(['currentUser', 'broadcast'], state); 86 | export default connect(mapStateToProps, mapDispatchToProps)(ProducerHeader); 87 | -------------------------------------------------------------------------------- /src/components/Dashboard/components/EventActions.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import R from 'ramda'; 4 | import classNames from 'classnames'; 5 | import { connect } from 'react-redux'; 6 | import { Link } from 'react-router'; 7 | import Icon from 'react-fontawesome'; 8 | import { deleteBroadcastEvent, updateStatus } from '../../../actions/events'; 9 | 10 | /** Event Actions */ 11 | type BaseProps = { event: BroadcastEvent }; 12 | type DispatchProps = { deleteEvent: BroadcastEvent => void, closeEvent: string => void }; 13 | type Props = BaseProps & DispatchProps; 14 | const EventActions = ({ event, deleteEvent, closeEvent }: Props): ReactComponent => { 15 | const style = (color: string): string => classNames('btn', 'action', color); 16 | const { id, status, archiveUrl, uncomposed } = event; 17 | 18 | const start = (): ReactComponent => 19 | 20 | 21 | ; 22 | 23 | const edit = (): ReactComponent => 24 | 25 | 26 | ; 27 | 28 | const del = (): ReactComponent => 29 | ; 32 | 33 | const view = (): ReactComponent => 34 | 35 | 36 | ; 37 | 38 | const end = (): ReactComponent => 39 | ; 42 | 43 | const close = (): ReactComponent => 44 | ; 47 | 48 | const download = (): ReactComponent => 49 | ; 52 | 53 | const viewArchive = (): ReactComponent => 54 | 55 | 58 | ; 59 | 60 | 61 | const actionButtons = (): ReactComponent[] => { 62 | switch (status) { 63 | case 'notStarted': 64 | return [start(), edit(), del()]; 65 | case 'preshow': 66 | return [view(), close()]; 67 | case 'live': 68 | return [view(), end()]; 69 | case 'closed': 70 | return R.isNil(archiveUrl) ? [] : [uncomposed ? download() : viewArchive()]; 71 | default: 72 | return []; 73 | } 74 | }; 75 | 76 | return ( 77 |
    78 | { actionButtons() } 79 |
    80 | ); 81 | }; 82 | 83 | const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch): DispatchProps => 84 | ({ 85 | deleteEvent: (event: BroadcastEvent) => { 86 | dispatch(deleteBroadcastEvent(event)); 87 | }, 88 | closeEvent: (id: string) => { 89 | dispatch(updateStatus(id, 'closed')); 90 | }, 91 | }); 92 | 93 | export default connect(null, mapDispatchToProps)(EventActions); 94 | -------------------------------------------------------------------------------- /src/components/UpdateEvent/components/EventForm.css: -------------------------------------------------------------------------------- 1 | /*.hidden{ 2 | display: none; 3 | } 4 | .rounded{ 5 | border-radius: 4px; 6 | }*/ 7 | 8 | .EventForm { 9 | position: relative; 10 | width: 90%; 11 | background-color: white; 12 | display: flex; 13 | flex-direction: column; 14 | align-items: flex-start; 15 | margin: 0 auto; 16 | padding: 20px 20px; 17 | } 18 | 19 | .EventForm .input-container { 20 | position: relative; 21 | width: 100%; 22 | display: flex; 23 | align-items: center; 24 | margin: 10px 0; 25 | } 26 | 27 | .EventForm .error-message-container { 28 | position: absolute; 29 | top: 34%; 30 | right: 6%; 31 | margin: 0 auto; 32 | width: 63%; 33 | height: 22px; 34 | display: flex; 35 | justify-content: center; 36 | align-items: center; 37 | color: red; 38 | font-weight: 600; 39 | font-style: italic; 40 | } 41 | 42 | .EventForm .input-container .label { 43 | width: 30%; 44 | text-align: left; 45 | color: #58666E; 46 | font-size: 13px; 47 | } 48 | 49 | .EventForm .input-container .icon { 50 | position: absolute; 51 | top: 10px; 52 | left: 31%; 53 | z-index: 2; 54 | } 55 | 56 | .EventForm .input-container input { 57 | height: 34px; 58 | width: 65%; 59 | font-size: 13px; 60 | border: 1px solid #BDC4C9; 61 | border-radius: 3px; 62 | box-shadow: inset 0px 1px 0px #F1F0F1; 63 | margin: 0 5px; 64 | padding: 6px 12px 6px 30px; 65 | } 66 | 67 | @-moz-document url-prefix() { 68 | .EventForm .input-container input { 69 | padding-top: 0; 70 | } 71 | } 72 | 73 | .EventForm .input-container .date-time-container { 74 | display: flex; 75 | width: 66%; 76 | justify-content: space-between; 77 | align-items: center; 78 | } 79 | 80 | .EventForm .input-container .date-time-container .time-selection { 81 | position: relative; 82 | width: 47%; 83 | } 84 | 85 | .EventForm .input-container .date-time-container .time-selection.error input { 86 | border-color: red; 87 | } 88 | 89 | .EventForm .input-container .date-time-container .time-selection .icon { 90 | left: 3.5%; 91 | } 92 | 93 | .EventForm .input-container .date-time-container .time-selection .rdt { 94 | margin: 0 5px; 95 | } 96 | 97 | .EventForm .input-container .date-time-container .time-selection input { 98 | width: 100%; 99 | margin: 0; 100 | } 101 | 102 | .EventForm .form-divider { 103 | width: 100%; 104 | border-bottom: 1px solid #BDC4C9; 105 | padding: 5px 0; 106 | } 107 | 108 | .EventForm .form-divider h4 { 109 | width: 100%; 110 | color: #58666E; 111 | text-align: left; 112 | margin-bottom: 0; 113 | } 114 | 115 | 116 | /*.EventForm .input-container input[type=url] { 117 | font-weight: 700; 118 | }*/ 119 | 120 | .EventForm .input-container.disabled input { 121 | font-weight: 700; 122 | background-color: rgba(211, 211, 211, .3); 123 | } 124 | 125 | .EventForm .input-container .btn.copy { 126 | position: absolute; 127 | right: 4.5%; 128 | top: calc(50% - 17px); 129 | width: 185px; 130 | height: 34px; 131 | font-weight: 300; 132 | border: 1px solid #BDC4C9; 133 | border-top-left-radius: 0; 134 | border-bottom-left-radius: 0; 135 | justify-content: flex-start; 136 | } 137 | 138 | .EventForm .input-container .btn.copy .icon { 139 | position: static; 140 | margin: 0 10px; 141 | } 142 | 143 | .EventForm .input-container.checkbox, 144 | .EventForm .input-container.submit { 145 | width: 65%; 146 | left: 30%; 147 | } 148 | 149 | .EventForm .input-container.submit button[disabled] { 150 | background-color: lightgray; 151 | cursor: not-allowed; 152 | } 153 | 154 | .EventForm .input-container.checkbox input { 155 | width: auto; 156 | height: 34px; 157 | border: none; 158 | box-shadow: none; 159 | } 160 | 161 | .EventForm .input-container.checkbox .label { 162 | color: #607d8b; 163 | font-size: 14px; 164 | font-weight: 300; 165 | width: initial; 166 | margin-left: 5px; 167 | } 168 | 169 | .EventForm .event-image-preview { 170 | height: 200px; 171 | display: flex; 172 | justify-content: center; 173 | align-items: center; 174 | position: relative; 175 | left: 30%; 176 | padding: 10px; 177 | } 178 | 179 | .EventForm .event-image-preview img { 180 | height: 100%; 181 | width: auto; 182 | } -------------------------------------------------------------------------------- /flowtypes/types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | /* eslint no-undef: "off" */ 4 | /* beautify preserve:start */ 5 | 6 | // API 7 | // declare type RequestHeaders = { 'Content-Type': 'application/json', jwt?: string }; 8 | // declare class Headers { 9 | // constructor(values: HeaderValues): void 10 | // } 11 | type HeadersInit = Headers | {[key: string]: string}; 12 | declare class Headers { 13 | @@iterator(): Iterator<[string, string]>; 14 | constructor(init?: HeadersInit): void; 15 | append(name: string, value: string): void; 16 | delete(name: string): void; 17 | entries(): Iterator<[string, string]>; 18 | get(name: string): string; 19 | getAll(name: string): Array; 20 | has(name: string): boolean; 21 | keys(): Iterator; 22 | set(name: string, value: string): void; 23 | values(): Iterator; 24 | } 25 | declare type HttpMethod = 'get' | 'GET' | 'post' | 'POST' | 'put' | 'PUT' | 'patch' | 'PATCH' | 'delete' | 'DELETE'; 26 | 27 | // Redux state(s) 28 | declare type State = { 29 | currentUser: CurrentUserState, 30 | users: UserMap, 31 | events: BroadcastEventMap, 32 | auth: AuthState, 33 | fan: FanState, 34 | broadcast: BroadcastState 35 | }; 36 | 37 | // What persists in local storage 38 | declare type LocalStorageState = { 39 | currentUser?: CurrentUserState 40 | } 41 | 42 | // Redux Actions 43 | declare type Action = 44 | AuthAction | 45 | UserAction | 46 | ManageUsersAction | 47 | EventsAction | 48 | BroadcastAction | 49 | FanAction | 50 | AlertAction; 51 | 52 | // Redux dispatch, action creators, etc. 53 | declare type ActionCreator = (*) => Action; 54 | declare type Dispatch = (action: Action | Thunk | Array) => any; // eslint-disable-line flowtype/no-weak-types 55 | declare type GetState = () => State; 56 | declare type Thunk = (dispatch: Dispatch, getState: GetState) => any; // eslint-disable-line flowtype/no-weak-types 57 | declare type ThunkActionCreator = (...*) => Thunk; 58 | 59 | // React Component 60 | declare type ReactComponent = React$Element<*> | React.CElement | null; 61 | 62 | declare type Route = { 63 | props: { 64 | component?: ReactClass<*>, 65 | render?: (router: Object) => React$Element<*>, // eslint-disable-line flowtype/no-weak-types 66 | children?: (router: Object) => React$Element<*>, // eslint-disable-line flowtype/no-weak-types 67 | path?: string, 68 | exact?: boolean, 69 | strict?: boolean 70 | } 71 | } 72 | 73 | 74 | // Functions 75 | declare type Unit = () => void; 76 | declare type AsyncVoid = Promise 77 | 78 | 79 | // Forms 80 | declare type FormErrors = null | { fields: { [field: string]: string, message: string } }; 81 | 82 | /** 83 | * Boilerplate React & Redux Types 84 | */ 85 | 86 | // http://www.saltycrane.com/blog/2016/06/flow-type-cheat-sheet/#lib/react.js 87 | // React 88 | declare class SyntheticEvent { 89 | bubbles: boolean, 90 | cancelable: boolean, 91 | currentTarget: EventTarget, 92 | defaultPrevented: boolean, 93 | eventPhase: number, 94 | isDefaultPrevented(): boolean, 95 | isPropagationStopped(): boolean, 96 | isTrusted: boolean, 97 | nativeEvent: Event, 98 | preventDefault(): void, 99 | stopPropagation(): void, 100 | +target: EventTarget, 101 | timeStamp: number, 102 | type: string, 103 | persist(): void 104 | } 105 | declare class SyntheticInputEvent extends SyntheticEvent { 106 | +target: HTMLInputElement, 107 | data: any // eslint-disable-line flowtype/no-weak-types 108 | } 109 | 110 | // Redux 111 | declare type Reducer = (state: S, action: A) => S; 112 | declare type Store = { 113 | dispatch: Dispatch, 114 | getState(): S, 115 | subscribe(listener: () => void): () => void, 116 | replaceReducer(nextReducer: Reducer): void 117 | }; 118 | 119 | // https://github.com/flowtype/flow-typed/blob/master/definitions/npm/react-redux_v5.x.x/flow_v0.30.x-/react-redux_v5.x.x.js 120 | // It's probably easier to use in-line type definitions for these: 121 | // eslint-disable-next-line flowtype/no-weak-types 122 | declare type MapStateToProps = (state: S, ownProps: OP) => SP | MapStateToProps; 123 | // eslint-disable-next-line flowtype/no-weak-types 124 | declare type MapDispatchToProps = ((dispatch: Dispatch) => DP) | DP; // Modified to use single type 125 | declare type MapDispatchWithOwn = ((dispatch: Dispatch, ownProps: OP) => DP); // eslint-disable-line flowtype/no-weak-types 126 | 127 | -------------------------------------------------------------------------------- /src/components/Common/Chat.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import R from 'ramda'; 4 | import { connect } from 'react-redux'; 5 | import classNames from 'classnames'; 6 | import Icon from 'react-fontawesome'; 7 | import { properCase } from '../../services/util'; 8 | import { sendChatMessage, minimizeChat, displayChat } from '../../actions/broadcast'; 9 | import './Chat.css'; 10 | 11 | const Message = (message: ChatMessage): ReactComponent => { 12 | const { isMe } = message; 13 | const messageClass = classNames('Message', { isMe }); 14 | return ( 15 |
    16 |
    17 | { message.text } 18 |
    19 |
    20 | ); 21 | }; 22 | 23 | type BaseProps = { 24 | chat: ChatState, 25 | actions?: ReactComponent 26 | }; 27 | 28 | type DispatchProps ={ 29 | sendMessage: (ChatMessagePartial) => void, 30 | minimize: Unit, 31 | hide: Unit 32 | }; 33 | 34 | type Props = BaseProps & DispatchProps; 35 | 36 | class Chat extends Component { 37 | 38 | props: Props; 39 | state: { newMessageText: string }; 40 | messageContainer: HTMLDivElement; 41 | updateScrollPosition: Unit; 42 | handleChange: SyntheticInputEvent => void; 43 | handleSubmit: SyntheticInputEvent => void; 44 | 45 | constructor(props: Props) { 46 | super(props); 47 | this.state = { newMessageText: '' }; 48 | this.updateScrollPosition = this.updateScrollPosition.bind(this); 49 | this.handleChange = this.handleChange.bind(this); 50 | this.handleSubmit = this.handleSubmit.bind(this); 51 | } 52 | 53 | componentWillReceiveProps(nextProps: Props) { 54 | const newMessages = nextProps.chat.messages; 55 | const { messages } = this.props.chat; 56 | if (newMessages.length > messages.length) { 57 | this.updateScrollPosition(); 58 | } 59 | } 60 | 61 | handleChange(e: SyntheticInputEvent) { 62 | const newMessageText = e.target.value; 63 | this.setState({ newMessageText }); 64 | } 65 | 66 | updateScrollPosition() { 67 | const { messageContainer } = this; 68 | setTimeout(() => { 69 | messageContainer.scrollTop = messageContainer.scrollHeight; 70 | }, 0); 71 | } 72 | 73 | handleSubmit(e: SyntheticInputEvent) { 74 | e.preventDefault(); 75 | const { newMessageText } = this.state; 76 | if (R.isEmpty(newMessageText)) { return; } 77 | 78 | const { sendMessage, chat } = this.props; 79 | 80 | const message = { 81 | text: newMessageText, 82 | timestamp: Date.now(), 83 | fromType: chat.fromType, 84 | fromId: chat.fromId, 85 | }; 86 | sendMessage(message); 87 | this.setState({ newMessageText: '' }, this.updateScrollPosition); 88 | } 89 | 90 | render(): ReactComponent { 91 | const { displayed, minimized, messages, toType, to } = this.props.chat; 92 | const name = R.prop('name', to); 93 | const { minimize, hide } = this.props; 94 | const { newMessageText } = this.state; 95 | const { handleSubmit, handleChange } = this; 96 | 97 | const ChatActions = R.propOr(null, 'actions', this.props); 98 | 99 | const chattingWithActiveFan = R.equals(toType, 'activeFan'); 100 | const chattingWith = chattingWithActiveFan ? 101 | properCase(name) : 102 | R.cond([ 103 | [R.equals('backstageFan'), R.always(`Backstage Fan - ${name}`)], 104 | [R.equals('fan'), R.always(`Fan - ${name}`)], 105 | [R.T, R.always(properCase(toType))], 106 | ])(toType); 107 | const inPrivateCall = R.and(chattingWithActiveFan, R.prop('inPrivateCall', this.props.chat)); 108 | return ( 109 |
    110 |
    111 | 112 | 113 |
    114 | { ChatActions } 115 |
    116 | { !minimized && 117 |
    118 |
    { this.messageContainer = el; }} > 119 | { R.map(Message, messages) } 120 |
    121 |
    122 | 129 | 130 |
    131 |
    132 | } 133 |
    134 | ); 135 | } 136 | } 137 | 138 | const mapDispatchToProps: MapDispatchWithOwn = (dispatch: Dispatch, ownProps: BaseProps): DispatchProps => ({ 139 | sendMessage: (message: ChatMessagePartial): void => dispatch(sendChatMessage(ownProps.chat.chatId, message)), 140 | minimize: (): void => dispatch(minimizeChat(ownProps.chat.chatId, !ownProps.chat.minimized)), 141 | hide: (): void => dispatch(displayChat(ownProps.chat.chatId, false)), 142 | }); 143 | 144 | export default connect(null, mapDispatchToProps)(Chat); 145 | -------------------------------------------------------------------------------- /src/components/Broadcast/CelebrityHost/CelebrityHost.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* eslint no-unused-vars: "off" */ 3 | import React, { Component } from 'react'; 4 | import R from 'ramda'; 5 | import { connect } from 'react-redux'; 6 | import { withRouter } from 'react-router'; 7 | import { toastr } from 'react-redux-toastr'; 8 | import classNames from 'classnames'; 9 | import { validateUser } from '../../../actions/auth'; 10 | import { initializeBroadcast, eventStarted } from '../../../actions/celebrityHost'; 11 | import { setBroadcastState, publishOnly, setBroadcastEventStatus, startCountdown } from '../../../actions/broadcast'; 12 | import { setInfo, resetAlert } from '../../../actions/alert'; 13 | import CelebrityHostHeader from './components/CelebrityHostHeader'; 14 | import CelebrityHostBody from './components/CelebrityHostBody'; 15 | import Loading from '../../../components/Common/Loading'; 16 | import NoEvents from '../../../components/Common/NoEvents'; 17 | import Chat from '../../../components/Common/Chat'; 18 | import NetworkReconnect from '../../Common/NetworkReconnect'; 19 | import { disconnect } from '../../../services/opentok'; 20 | import './CelebrityHost.css'; 21 | 22 | /* beautify preserve:start */ 23 | 24 | type InitialProps = { params: { hostUrl: string, celebrityUrl: string, adminId: string } }; 25 | type BaseProps = { 26 | adminId: string, 27 | userType: 'host' | 'celebrity', 28 | userUrl: string, 29 | broadcast: BroadcastState, 30 | disconnected: boolean, 31 | authError: Error, 32 | isEmbed: boolean, 33 | eventStarted: boolean 34 | }; 35 | type DispatchProps = { 36 | init: CelebHostInitOptions => void, 37 | changeEventStatus: (event: EventStatus) => void, 38 | togglePublishOnly: (enable: boolean) => void, 39 | startEvent: () => void 40 | }; 41 | type Props = InitialProps & BaseProps & DispatchProps; 42 | /* beautify preserve:end */ 43 | 44 | const newBackstageFan = (): void => toastr.info('A new FAN has been moved to backstage', { showCloseButton: false }); 45 | 46 | class CelebrityHost extends Component { 47 | 48 | props: Props; 49 | init: Unit; 50 | changeEventStatus: Unit; 51 | signalListener: SignalListener; 52 | 53 | componentDidMount() { 54 | const { adminId, userType, userUrl, init } = this.props; 55 | const options = { 56 | adminId, 57 | userType, 58 | userUrl, 59 | }; 60 | init(options); 61 | } 62 | 63 | startEvent = () => { 64 | const { startEvent } = this.props; 65 | startEvent(); 66 | } 67 | 68 | componentWillReceiveProps(nextProps: Props) { 69 | if (R.pathEq(['broadcast', 'event', 'status'], 'closed', nextProps)) { disconnect(); } 70 | } 71 | 72 | render(): ReactComponent { 73 | const { userType, togglePublishOnly, broadcast, disconnected, authError, isEmbed, eventStarted } = this.props; 74 | const { event, participants, publishOnlyEnabled, privateCall, chats } = broadcast; 75 | const producerChat = R.prop('producer', chats); 76 | if (authError) return ; 77 | if (!event) return ; 78 | const availableParticipants = publishOnlyEnabled ? null : participants; 79 | const mainClassNames = classNames('CelebrityHost', { CelebrityHostEmbed: isEmbed }); 80 | return ( 81 |
    82 | 83 |
    84 | 94 | 102 |
    103 | { producerChat && } 104 |
    105 |
    106 |
    107 | ); 108 | } 109 | } 110 | 111 | const mapStateToProps = (state: State, ownProps: InitialProps): BaseProps => { 112 | const { hostUrl, celebrityUrl } = ownProps.params; 113 | return { 114 | adminId: R.path(['params', 'adminId'], ownProps), 115 | userType: R.path(['route', 'userType'], ownProps), 116 | isEmbed: R.path(['route', 'embed'], ownProps), 117 | userUrl: hostUrl || celebrityUrl, 118 | broadcast: R.prop('broadcast', state), 119 | disconnected: R.path(['broadcast', 'disconnected'], state), 120 | authError: R.path(['auth', 'error'], state), 121 | eventStarted: R.path(['broadcast', 'eventStarted'], state), 122 | }; 123 | }; 124 | 125 | const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch): DispatchProps => 126 | ({ 127 | init: (options: CelebHostInitOptions): void => dispatch(initializeBroadcast(options)), 128 | changeEventStatus: (status: EventStatus): void => dispatch(setBroadcastEventStatus(status)), 129 | showCountdown: (): void => dispatch(startCountdown()), 130 | togglePublishOnly: (enable: boolean): void => dispatch(publishOnly()), 131 | startEvent: (): void => dispatch(eventStarted()), 132 | }); 133 | 134 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(CelebrityHost)); 135 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | devrel@vonage.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. -------------------------------------------------------------------------------- /src/services/api.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import R from 'ramda'; 3 | import { loadAuthToken as jwt } from './localStorage'; 4 | import api from '../config/api'; 5 | 6 | /** Constants */ 7 | const origin = window.location.origin; 8 | const url = R.contains('localhost', origin) ? api.localhost : api.production; 9 | const apiUrl = `${url}/api`; 10 | const defaultHeaders = { 11 | 'Content-Type': 'application/json', 12 | 'cache-control': 'no-cache', 13 | pragma: 'no-cache', 14 | }; 15 | /** ********* */ 16 | 17 | /** Generator headers for a request */ 18 | const headers = (requiresAuth: boolean, authToken?: string): Headers => 19 | R.merge(defaultHeaders, requiresAuth ? { Authorization: `Bearer ${authToken || jwt()}` } : null); 20 | 21 | /** Check for external route containing http/https */ 22 | const getURL = (route: string): string => route.includes('http') ? route : `${apiUrl}/${route}`; 23 | 24 | /** Parse a response based on the type */ 25 | const parseResponse = (response: Response): Promise<*> => { 26 | const contentType = R.head(R.split(';')(R.defaultTo('')(response.headers.get('content-type')))); 27 | if (contentType === 'application/json') { 28 | return response.json(); 29 | } 30 | return response.text(); // contentType === 'text/html' 31 | }; 32 | 33 | /** Parse API error response */ 34 | const parseErrorResponse = (response: Response): Promise => response.json(); 35 | 36 | /** Check for API-level errors */ 37 | const checkStatus = (response: Response): Promise<*> => 38 | new Promise((resolve: Promise.resolve, reject: Promise.reject) => { 39 | if (response.status >= 200 && response.status < 300) { // $FlowFixMe 40 | resolve(response); 41 | } else { 42 | parseErrorResponse(response) // $FlowFixMe 43 | .then(({ message }: { message: string }): void => reject(new Error(message))) // $FlowFixMe 44 | .catch(reject); 45 | } 46 | }); 47 | 48 | /** Create a new Request object */ 49 | const request = (method: HttpMethod, route: string, data?: *, requiresAuth: boolean = true, authToken?: string): Request => { 50 | const body = (): {} | { body: string } => data ? { body: JSON.stringify(data) } : {}; 51 | const baseOptions = { 52 | method: method.toUpperCase(), 53 | mode: 'cors', 54 | headers: new Headers(headers(requiresAuth, authToken)), 55 | }; 56 | const requestOptions = R.merge(baseOptions, body()); 57 | return new Request(getURL(route), requestOptions); 58 | }; 59 | 60 | /** Execute a request using fetch */ 61 | const execute = (method: HttpMethod, route: string, body: * = null, requiresAuth: boolean = true, authToken?: string): Promise<*> => 62 | new Promise((resolve: Promise.resolve<>, reject: Promise.reject<>) => { 63 | fetch(request(method, route, body, requiresAuth, authToken)) 64 | .then(checkStatus) 65 | .then(parseResponse) 66 | .then(resolve) 67 | .catch(reject); 68 | }); 69 | 70 | /** HTTP Methods */ 71 | const get = (route: string, requiresAuth: boolean = true, authToken?: string): Promise<*> => 72 | execute('get', route, null, requiresAuth, authToken); 73 | const post = (route: string, body: * = null, requiresAuth: boolean = true, authToken?: string): Promise<*> => 74 | execute('post', route, body, requiresAuth, authToken); 75 | const put = (route: string, body: * = null, requiresAuth: boolean = true): Promise<*> => execute('put', route, body, requiresAuth); 76 | const patch = (route: string, body: * = null, requiresAuth: boolean = true): Promise<*> => execute('patch', route, body, requiresAuth); 77 | const del = (route: string, requiresAuth: boolean = true): Promise<*> => execute('delete', route, null, requiresAuth); 78 | 79 | /** Exports */ 80 | 81 | /** Auth */ 82 | const getAuthTokenUser = (adminId: string, userType: string, userUrl: string): Promise<{token: AuthToken}> => 83 | post(`auth/token-${userType}`, R.assoc(`${userType}Url`, userUrl, { adminId }), false); 84 | const getAuthToken = (idToken: string): Promise<{ token: AuthToken }> => post('auth/token', { idToken }, false); 85 | 86 | /** User */ 87 | const getUser = (userId: string): Promise => get(`admin/${userId}`); 88 | const createUser = (userData: UserFormData): Promise => post('admin', userData); 89 | const updateUser = (userData: UserUpdateFormData): Promise => patch(`admin/${userData.id}`, userData); 90 | const getAllUsers = (): Promise<[User]> => get('admin'); 91 | const deleteUserRecord = (userId: string): Promise => del(`admin/${userId}`); 92 | 93 | /** Events */ 94 | const getEvents = (adminId: string): Promise => get(`event?adminId=${adminId}`); 95 | const getEvent = (id: string): Promise => get(`event/${id}`); 96 | const createEvent = (data: BroadcastEventFormData): Promise => post('event', data); 97 | const updateEvent = (data: BroadcastEventUpdateFormData): Promise => patch(`event/${data.id}`, data); 98 | const updateEventStatus = (id: string, status: EventStatus): Promise => put(`event/change-status/${id}`, { status }); 99 | const deleteEvent = (id: string): Promise => del(`event/${id}`); 100 | const getMostRecentEvent = (id: string): Promise => get(`event/get-current-admin-event?adminId=${id}`); 101 | const getAdminCredentials = (eventId: EventId): Promise => post(`event/create-token-producer/${eventId}`); 102 | const getEventWithCredentials = (data: { adminId: UserId, userType: UserRole }, authToken: AuthToken): Promise => 103 | post(`event/create-token-${data.userType}`, data, true, authToken); 104 | const getEmbedEventWithCredentials = (data: { adminId: UserId, userType: UserRole }, authToken: AuthToken): Promise => 105 | post(`event/create-token/${data.adminId}/${data.userType}`, data, true, authToken); 106 | /** Exports */ 107 | 108 | module.exports = { 109 | getAuthToken, 110 | getAuthTokenUser, 111 | getUser, 112 | createUser, 113 | updateUser, 114 | getAllUsers, 115 | getEvent, 116 | getEvents, 117 | getMostRecentEvent, 118 | createEvent, 119 | updateEvent, 120 | updateEventStatus, 121 | deleteEvent, 122 | getAdminCredentials, 123 | deleteUserRecord, 124 | url, 125 | getEventWithCredentials, 126 | getEmbedEventWithCredentials, 127 | }; 128 | -------------------------------------------------------------------------------- /src/components/Broadcast/Producer/components/Participant.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { connect } from 'react-redux'; 4 | import R from 'ramda'; 5 | import classNames from 'classnames'; 6 | import Icon from 'react-fontawesome'; 7 | import CopyToClipboard from '../../../Common/CopyToClipboard'; 8 | import createUrls from '../../../../services/eventUrls'; 9 | import { isFan } from '../../../../services/util'; 10 | import ControlIcon from './ControlIcon'; 11 | import { toggleParticipantProperty } from '../../../../actions/broadcast'; 12 | import { connectPrivateCall, chatWithParticipant, sendToStage, kickFanFromFeed } from '../../../../actions/producer'; 13 | import './Participant.css'; 14 | 15 | const isBackstageFan = R.equals('backstageFan'); 16 | const isOnStageFan = R.equals('fan'); 17 | const getHeaderLabel = (type: ParticipantType): string => R.toUpper(isBackstageFan(type) ? 'backstage fan' : type); 18 | 19 | type OwnProps = { 20 | type: ParticipantType 21 | }; 22 | 23 | type BaseProps = { 24 | broadcast: BroadcastState, 25 | fanRecord: ActiveFan | null 26 | }; 27 | 28 | type DispatchProps = { 29 | toggleAudio: Unit, 30 | toggleVideo: Unit, 31 | toggleVolume: Unit, 32 | privateCall: Unit, 33 | chat: Unit, 34 | kickFan: Unit, 35 | sendFanToStage: Unit 36 | }; 37 | 38 | type Props = OwnProps & BaseProps & DispatchProps; 39 | 40 | const Participant = (props: Props): ReactComponent => { 41 | const { type, toggleAudio, toggleVideo, toggleVolume, privateCall, chat, kickFan, broadcast, sendFanToStage, fanRecord } = props; 42 | const fanId = R.prop('id', fanRecord || {}); 43 | const url = R.prop(`${type}Url`, createUrls(broadcast.event || {})); 44 | const me = R.prop(type, broadcast.participants) || {}; 45 | const stageCountdown = broadcast.stageCountdown; 46 | const inPrivateCall = R.pathEq(['privateCall', 'isWith'], type, broadcast); 47 | const availableForPrivateCall = (): boolean => { 48 | const inPreshow = R.pathEq(['event', 'status'], 'preshow', broadcast); 49 | return (me.connected && (inPreshow || isBackstageFan(type))); 50 | }; 51 | const statusIconClass = classNames('icon', { green: me.connected }); 52 | const controlIconClass = classNames('icon', { active: me.connected }); 53 | const volumeIconDisabled = (!inPrivateCall && isBackstageFan(type)) || !me.connected; 54 | const volumeIconClass = classNames('icon', { active: !volumeIconDisabled }); 55 | const privateCallIconClass = classNames('icon', { active: me.connected && availableForPrivateCall() }); 56 | const status = me.connected ? 'Online' : 'Offline'; 57 | return ( 58 |
    59 |
    60 | { getHeaderLabel(type) } 61 | {status} 62 |
    63 |
    64 | { !me.audio && me.connected &&
    MUTED
    } 65 | { isOnStageFan(type) && stageCountdown >= 0 && 66 |
    67 | {stageCountdown} 68 |
    69 | } 70 |
    71 | { isBackstageFan(type) ? 72 |
    73 | 74 |
    : 75 |
    76 | { url } 77 | 78 | 79 | 80 |
    81 | } 82 |
    83 | Alter Feed 84 |
    85 | 91 | 97 | 103 | 109 | { R.contains('fan', R.toLower(type)) ? 110 | : 111 | 112 | } 113 |
    114 |
    115 |
    116 | ); 117 | }; 118 | 119 | const mapStateToProps = (state: State, ownProps: OwnProps): BaseProps => ({ 120 | broadcast: R.prop('broadcast', state), 121 | fanRecord: isFan(ownProps.type) ? R.path(['broadcast', 'participants', ownProps.type, 'record'], state) : null, 122 | }); 123 | 124 | const mapDispatchToProps: MapDispatchWithOwn = (dispatch: Dispatch, ownProps: OwnProps): DispatchProps => ({ 125 | toggleAudio: (): void => dispatch(toggleParticipantProperty(ownProps.type, 'audio')), 126 | toggleVideo: (): void => dispatch(toggleParticipantProperty(ownProps.type, 'video')), 127 | toggleVolume: (): void => dispatch(toggleParticipantProperty(ownProps.type, 'volume')), 128 | privateCall: (fanId?: UserId): void => dispatch(connectPrivateCall(ownProps.type, fanId)), 129 | kickFan: (): void => dispatch(kickFanFromFeed(ownProps.type)), 130 | chat: (): void => dispatch(chatWithParticipant(ownProps.type)), 131 | sendFanToStage: (): void => dispatch(sendToStage()), 132 | }); 133 | 134 | 135 | export default connect(mapStateToProps, mapDispatchToProps)(Participant); 136 | -------------------------------------------------------------------------------- /src/components/UpdateEvent/UpdateEvent.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import R from 'ramda'; 4 | import { connect } from 'react-redux'; 5 | import { withRouter, Link } from 'react-router'; 6 | import moment from 'moment'; 7 | import { createBroadcastEvent, updateBroadcastEvent, getBroadcastEvents, displayNoApiKeyAlert } from '../../actions/events'; 8 | import EventForm from './components/EventForm'; 9 | import './UpdateEvent.css'; 10 | 11 | /* beautify preserve:start */ 12 | type InitialProps = { params: { id?: EventId } }; 13 | type BaseProps = { 14 | user: CurrentUserState, 15 | events: null | BroadcastEventMap, 16 | eventId: null | EventId 17 | }; 18 | type DispatchProps = { 19 | loadEvents: UserId => void, 20 | createEvent: BroadcastEventFormData => void, 21 | updateEvent: BroadcastEventUpdateFormData => void, 22 | noApiKeyAlert: Unit 23 | }; 24 | type Props = InitialProps & BaseProps & DispatchProps; 25 | /* beautify preserve:end */ 26 | 27 | class UpdateEvent extends Component { 28 | props: Props; 29 | state: { 30 | errors: null | { fields: { [field: string]: boolean }, message: string }, 31 | dateTimeSet: boolean 32 | }; 33 | onUpdate: string => void; 34 | onSubmit: BroadcastEventFormData => void; 35 | validateAndFormat: BroadcastEventFormData => null | BroadcastEventFormData; 36 | constructor(props: Props) { 37 | super(props); 38 | this.state = { 39 | errors: null, 40 | dateTimeSet: false 41 | }; 42 | this.validateAndFormat = this.validateAndFormat.bind(this); 43 | this.onSubmit = this.onSubmit.bind(this); 44 | this.onUpdate = this.onUpdate.bind(this); 45 | } 46 | componentDidMount() { 47 | const { user } = this.props; 48 | if (!this.props.events) { 49 | this.props.loadEvents(user.id); 50 | } 51 | if (!user.otApiKey) { 52 | this.props.noApiKeyAlert(); 53 | } 54 | } 55 | 56 | onUpdate(field: string) { 57 | if (R.contains(field, ['dateTimeStart', 'dateTimeEnd'])) { 58 | this.setState({ dateTimeSet: true }); 59 | } 60 | const errorFields = R.pathOr({}, ['errors', 'fields'], this.state); 61 | if (errorFields[field]) { 62 | this.setState({ errors: null }); 63 | } 64 | } 65 | 66 | // TODO: Redirect URL validation 67 | validateAndFormat(data: BroadcastEventFormData): (null | BroadcastEventFormData) { 68 | 69 | // Ensure valid start and end times 70 | if (moment(new Date(data.dateTimeStart)).isAfter(new Date(data.dateTimeEnd))) { 71 | const errors = { 72 | fields: { dateTimeStart: true, dateTimeEnd: true }, 73 | message: 'Start time cannot be after end time.', 74 | }; 75 | this.setState({ errors }); 76 | return null; 77 | } 78 | 79 | // Empty fields should not be included 80 | const datesSet = this.state.dateTimeSet; 81 | const omitIfEmpty = (acc: string[], field: string): string[] => R.isEmpty(data[field]) ? R.append(field, acc) : acc; 82 | // Omit start and end images if they are null 83 | const omitIfNull = (acc: string[], image: string): string[] => R.isNil(data[image]) ? R.append(image, acc) : acc; 84 | const emptyImageFields = R.reduce(omitIfNull, [], ['startImage', 'endImage']); 85 | // If the admin has not set the dates/times, omit them 86 | const initialFields = datesSet ? [] : ['dateTimeStart', 'dateTimeEnd']; 87 | const fieldsToOmit = R.concat(R.reduce(omitIfEmpty, initialFields, R.keys(data)), emptyImageFields); 88 | 89 | 90 | // Standard moment formatting for timestamps. Slugs only for urls. 91 | 92 | const editTimestamp = (t: string): (string) => moment(new Date(t)).format(); 93 | const formatting = { 94 | name: R.compose(R.replace(/ +/g, ' '), R.trim), // eslint-disable-line no-regex-spaces 95 | dateTimeStart: editTimestamp, 96 | dateTimeEnd: editTimestamp, 97 | celebrityUrl: R.compose(R.last, R.split('/')), 98 | fanUrl: R.compose(R.last, R.split('/')), 99 | hostUrl: R.compose(R.last, R.split('/')), 100 | }; 101 | 102 | return R.compose( 103 | R.assoc('adminId', R.path(['user', 'id'], this.props)), 104 | R.evolve(formatting), 105 | R.omit(R.append('fanAudioUrl', fieldsToOmit)) // eslint-disable-line comma-dangle 106 | )(data); 107 | } 108 | 109 | onSubmit(data: BroadcastEventFormData) { 110 | 111 | const formattedData = this.validateAndFormat(data); 112 | 113 | if (!formattedData) { 114 | return; 115 | } 116 | 117 | const eventId = R.path(['params', 'id'], this.props); 118 | if (R.isNil(eventId)) { 119 | this.props.createEvent(formattedData); 120 | } else { 121 | this.props.updateEvent(R.assoc('id', eventId, formattedData)); 122 | } 123 | } 124 | 125 | render(): ReactComponent { 126 | const { onSubmit, onUpdate } = this; 127 | const { user, eventId } = this.props; 128 | const { errors } = this.state; 129 | const event = R.pathOr(null, ['events', eventId], this.props); 130 | return ( 131 |
    132 |
    133 | Back to Events 134 |

    { eventId ? 'Edit Event' : 'Add New Event' }

    135 |
    136 |
    137 | 138 |
    139 |
    140 | ); 141 | } 142 | } 143 | 144 | const mapStateToProps = (state: State, ownProps: InitialProps): BaseProps => ({ 145 | eventId: R.pathOr(null, ['params', 'id'], ownProps), 146 | events: R.path(['events', 'map'], state), 147 | user: state.currentUser, 148 | }); 149 | 150 | const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ 151 | loadEvents: (userId: UserId) => { 152 | dispatch(getBroadcastEvents(userId)); 153 | }, 154 | createEvent: (data: BroadcastEventFormData) => { 155 | dispatch(createBroadcastEvent(data)); 156 | }, 157 | updateEvent: (data: BroadcastEventUpdateFormData) => { 158 | dispatch(updateBroadcastEvent(data)); 159 | }, 160 | noApiKeyAlert: () => { 161 | dispatch(displayNoApiKeyAlert()); 162 | }, 163 | }); 164 | 165 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(UpdateEvent)); 166 | --------------------------------------------------------------------------------