├── .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 |
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 |
13 |
14 | ;
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 |
11 |
Active Fans ({ broadcast.activeFans.order.length })
12 |
13 |
;
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 |
13 | All Events
14 |
15 |
16 | Current Events
17 |
18 |
19 | Archived Events
20 |
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 | Logout
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 | Edit
18 | { !adminId && Delete }
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 |
19 | All Events
20 |
21 |
22 | Current Events
23 |
24 |
25 | Archived Events
26 |
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 | );
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 |
24 | Most Recent
25 |
26 |
27 | Start Date
28 |
29 |
30 | Event State
31 |
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 |
28 |
29 | }
30 | { !isClosed && !eventStarted &&
JOIN SESSION }
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 |
45 |
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 | PUBLISH ONLY {publishOnlyEnabled ? 'ON' : 'OFF'}
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 | Copy User ID
47 |
48 |
49 | Add New Event
50 |
51 | {
52 | currentUser.superAdmin &&
53 |
54 | Manage Users
55 |
56 | }
57 | {
58 | !currentUser.superAdmin &&
59 |
60 | My profile
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 && GET IN LINE :
44 | LEAVE LINE ;
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 |
54 | Get Embed Code
55 |
56 |
57 |
58 | Get fan code
59 |
60 |
61 | Get host code
62 |
63 |
64 | Get celebrity code
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 |
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 |
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 |
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 |
70 |
71 |
72 |
73 | { error &&
Please check your credentials and try again
}
74 |
75 | { forgotPassword ? 'Enter your email to reset your password.' : 'Forgot your password?' }
76 |
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 | COPY
46 |
47 |
48 |
49 |
50 | { status === 'live' && (archiving || archiveId) &&
51 |
52 | ARCHIVING
53 |
54 | }
55 | { status === 'preshow' && !disconnected &&
56 |
57 |
58 | { connected ? 'GO LIVE' : 'CONNECTING' }
59 |
60 | }
61 | { status === 'live' && !disconnected &&
62 |
63 |
64 | END SHOW
65 |
66 | }
67 |
68 | COPY ADMIN ID
69 |
70 |
71 |
72 |
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 | Start Event
21 | ;
22 |
23 | const edit = (): ReactComponent =>
24 |
25 | Edit
26 | ;
27 |
28 | const del = (): ReactComponent =>
29 |
30 | Delete
31 | ;
32 |
33 | const view = (): ReactComponent =>
34 |
35 | View Event
36 | ;
37 |
38 | const end = (): ReactComponent =>
39 |
40 | End Event
41 | ;
42 |
43 | const close = (): ReactComponent =>
44 |
45 | Close Event
46 | ;
47 |
48 | const download = (): ReactComponent =>
49 | { window.location = archiveUrl; }} >
50 | Download
51 | ;
52 |
53 | const viewArchive = (): ReactComponent =>
54 |
55 |
56 | Watch video
57 |
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 | Chat with { chattingWith }
112 |
113 |
114 | { ChatActions }
115 |
116 | { !minimized &&
117 |
118 |
{ this.messageContainer = el; }} >
119 | { R.map(Message, messages) }
120 |
121 |
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 | Move to fan feed
74 |
:
75 |
76 | { url }
77 |
78 | COPY
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 |
--------------------------------------------------------------------------------