mirror of
https://github.com/cpvalente/ontime.git
synced 2026-07-26 10:38:55 +00:00
Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f68b5f1d74 | |||
| db0eee3b9c | |||
| 20d3ddbc18 | |||
| 82d0508672 | |||
| dd1b9bd38c | |||
| b01145ca4b | |||
| 67ddcd8c68 | |||
| 8841a02754 | |||
| 1a0d7810c4 | |||
| f2dd24b74a | |||
| 6a6f68b2f9 | |||
| 906a6631a7 | |||
| 79a24a134e | |||
| acb5d8ae08 | |||
| 5ca8202368 | |||
| ba782d5d22 | |||
| 48093b8651 | |||
| c3f18feaae | |||
| 2fa397548a | |||
| dd43d5e20a | |||
| 9fc154955a | |||
| 160ccabebc | |||
| 4974898050 | |||
| ac0d5832b6 | |||
| 2b2bafa8c6 | |||
| fad34eeab2 | |||
| c12bca05aa | |||
| 1e4206fcb2 | |||
| 2751ed3d33 | |||
| cd911aaaa2 | |||
| 1321aa555e | |||
| 12e4eaef67 | |||
| 28c9cb0864 | |||
| afdfae9073 | |||
| 0736ce93c6 | |||
| 52f02f153c | |||
| 3582ce18e1 | |||
| 80c2e9a8ba | |||
| 627cce7ba6 | |||
| 8c740b7111 | |||
| bf8170a947 | |||
| eb2abb7026 | |||
| f3320efc5c | |||
| 17d5c84133 | |||
| 09a5aca25a | |||
| 885d965db1 | |||
| 26cb8a5bfc | |||
| 8520d08927 | |||
| 8d8bbef546 | |||
| 207612e3b8 | |||
| 98e5cddad5 | |||
| f6edb88944 | |||
| 3d2a79f92a | |||
| f73d8ae8f4 | |||
| 1138b4dd98 | |||
| ad89a780be | |||
| 9bc2923586 | |||
| 93580554bd | |||
| 03ef3e58ed | |||
| 4d2769dee4 | |||
| 0bb728d76c | |||
| 5a68d292e1 | |||
| 418f8018fe | |||
| 89d3bc967c | |||
| 144ce1c046 | |||
| 513e50a998 | |||
| f0ecb85c29 | |||
| c0e0631cd2 |
@@ -0,0 +1,18 @@
|
||||
version = 1
|
||||
|
||||
test_patterns = [
|
||||
"__mocks__/**",
|
||||
"__tests__/**",
|
||||
"cypress/**",
|
||||
"*.test.*",
|
||||
"*.mock.*",
|
||||
"*.spec.*"
|
||||
]
|
||||
|
||||
[[analyzers]]
|
||||
name = "javascript"
|
||||
enabled = true
|
||||
|
||||
[analyzers.meta]
|
||||
plugins = ["react"]
|
||||
environment = ["nodejs","browser","jest"]
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2020
|
||||
},
|
||||
"env": {
|
||||
"es6": true,
|
||||
"jest": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended"
|
||||
],
|
||||
"rules": {
|
||||
// disallow certain object properties
|
||||
// https://eslint.org/docs/rules/no-restricted-properties
|
||||
"no-restricted-properties": [
|
||||
"error",
|
||||
{
|
||||
"object": "global",
|
||||
"property": "isNaN",
|
||||
"message": "Please use Number.isNaN instead"
|
||||
},
|
||||
{
|
||||
"object": "self",
|
||||
"property": "isNaN",
|
||||
"message": "Please use Number.isNaN instead"
|
||||
},
|
||||
{
|
||||
"object": "window",
|
||||
"property": "isNaN",
|
||||
"message": "Please use Number.isNaN instead"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 248 KiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,141 @@
|
||||
name: Ontime build
|
||||
|
||||
on:
|
||||
push:
|
||||
# run when a tag is created
|
||||
tags:
|
||||
- '*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build_mac:
|
||||
runs-on: macOS-latest
|
||||
env:
|
||||
CI: ''
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14.x'
|
||||
|
||||
# React
|
||||
- name: React - Install dependencies
|
||||
run: yarn install
|
||||
working-directory: ./client
|
||||
|
||||
- name: React - Build project
|
||||
run: yarn build
|
||||
working-directory: ./client
|
||||
|
||||
# Node server
|
||||
- name: Server - Install dependencies
|
||||
run: yarn install
|
||||
working-directory: ./server/src
|
||||
|
||||
# App
|
||||
- name: Electron - Install dependencies
|
||||
shell: bash
|
||||
run: yarn install && yarn setdb
|
||||
working-directory: ./server
|
||||
- name: Electron - Build app
|
||||
run: yarn dist-mac
|
||||
working-directory: ./server
|
||||
|
||||
# Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: ./server/dist/ontime-macOS.dmg
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
build_win:
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
CI: ''
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '14.x'
|
||||
|
||||
# React
|
||||
- name: React - Install dependencies
|
||||
run: yarn install
|
||||
working-directory: ./client
|
||||
|
||||
- name: React - Build project
|
||||
run: yarn build
|
||||
working-directory: ./client
|
||||
|
||||
# Node server
|
||||
- name: Server - Install dependencies
|
||||
run: yarn install
|
||||
working-directory: ./server/src
|
||||
|
||||
# App
|
||||
- name: Electron - Install dependencies
|
||||
shell: bash
|
||||
run: yarn install && yarn setdb
|
||||
working-directory: ./server
|
||||
- name: Electron - Build app
|
||||
run: yarn dist-mac
|
||||
working-directory: ./server
|
||||
|
||||
# Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: ./server/dist/ontime-win64.exe
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# build_linux:
|
||||
# runs-on: ubuntu-latest
|
||||
# env:
|
||||
# CI: ''
|
||||
#
|
||||
# steps:
|
||||
# - uses: actions/checkout@v2
|
||||
# - name: Use Node.js
|
||||
# uses: actions/setup-node@v1
|
||||
# with:
|
||||
# node-version: '14.x'
|
||||
#
|
||||
# React
|
||||
# - name: React - Link to utils
|
||||
# run: yarn link ontime-utils
|
||||
# working-directory: ./client
|
||||
#
|
||||
# - name: React - Install dependencies
|
||||
# run: yarn install
|
||||
# working-directory: ./client
|
||||
#
|
||||
# - name: React - Build project
|
||||
# run: yarn build
|
||||
# working-directory: ./client
|
||||
#
|
||||
# Node server
|
||||
# - name: Server - Install dependencies
|
||||
# run: yarn install
|
||||
# working-directory: ./server/src
|
||||
#
|
||||
# App
|
||||
# - name: Electron - Install dependencies
|
||||
# run: yarn install && yarn make && yarn setdb
|
||||
# working-directory: ./server
|
||||
# - name: Electron - Build app
|
||||
# run: yarn dist-linux
|
||||
# working-directory: ./server
|
||||
#
|
||||
# Release
|
||||
# - name: Release
|
||||
# uses: softprops/action-gh-release@v1
|
||||
# with:
|
||||
# files: ./server/dist/ontime-win64.exe
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,70 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ master ]
|
||||
schedule:
|
||||
- cron: '33 02 * * 5'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
@@ -0,0 +1,53 @@
|
||||
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: ontime_test_CI
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CI: ''
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14.x'
|
||||
|
||||
# React
|
||||
- name: React - Install dependencies
|
||||
run: yarn install
|
||||
working-directory: ./client
|
||||
|
||||
- name: React - Run tests
|
||||
run: yarn test:pipeline
|
||||
working-directory: ./client
|
||||
|
||||
- name: React - Build project
|
||||
run: yarn build
|
||||
working-directory: ./client
|
||||
|
||||
# Node server
|
||||
- name: Server - Install dependencies
|
||||
run: yarn install
|
||||
working-directory: ./server/src
|
||||
|
||||
# App
|
||||
- name: Electron - Install dependencies
|
||||
run: yarn install && yarn setdb
|
||||
working-directory: ./server
|
||||
|
||||
- name: Electron - Run tests
|
||||
run: yarn test
|
||||
working-directory: ./server
|
||||
|
||||
- name: Cypress run
|
||||
uses: cypress-io/github-action@v2
|
||||
with:
|
||||
working-directory: ./server
|
||||
start: yarn cypress
|
||||
+10
-5
@@ -6,7 +6,8 @@ node_modules/
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
coverage/
|
||||
*.mp4
|
||||
|
||||
# production
|
||||
build/
|
||||
@@ -23,13 +24,17 @@ dist/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
ontime.code-workspace
|
||||
TODO.md
|
||||
|
||||
# working stuff
|
||||
_SS/
|
||||
.vscode/launch.json
|
||||
.eslintrc.json
|
||||
db backup.json
|
||||
server/src/data/db.json
|
||||
server/src/models/db.json
|
||||
TODO.md
|
||||
|
||||
# vscode stuff
|
||||
.vscode/*
|
||||
ontime.code-workspace
|
||||
|
||||
# webstorm stuff
|
||||
.idea/*
|
||||
Generated
+5
@@ -0,0 +1,5 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="CssUnknownProperty" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="myCustomPropertiesEnabled" value="true" />
|
||||
<option name="myIgnoreVendorSpecificProperties" value="false" />
|
||||
<option name="myCustomPropertiesList">
|
||||
<value>
|
||||
<list size="3">
|
||||
<item index="0" class="java.lang.String" itemvalue="scrollbar-color" />
|
||||
<item index="1" class="java.lang.String" itemvalue="scrollbar-width" />
|
||||
<item index="2" class="java.lang.String" itemvalue="app-region" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="HttpUrlsUsage" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="ignoredUrls">
|
||||
<list>
|
||||
<option value="http://localhost" />
|
||||
<option value="http://127.0.0.1" />
|
||||
<option value="http://0.0.0.0" />
|
||||
<option value="http://www.w3.org/" />
|
||||
<option value="http://json-schema.org/draft" />
|
||||
<option value="http://java.sun.com/" />
|
||||
<option value="http://xmlns.jcp.org/" />
|
||||
<option value="http://javafx.com/javafx/" />
|
||||
<option value="http://javafx.com/fxml" />
|
||||
<option value="http://maven.apache.org/xsd/" />
|
||||
<option value="http://maven.apache.org/POM/" />
|
||||
<option value="http://www.springframework.org/schema/" />
|
||||
<option value="http://www.springframework.org/tags" />
|
||||
<option value="http://www.springframework.org/security/tags" />
|
||||
<option value="http://www.thymeleaf.org" />
|
||||
<option value="http://www.jboss.org/j2ee/schema/" />
|
||||
<option value="http://www.jboss.com/xml/ns/" />
|
||||
<option value="http://www.ibm.com/webservices/xsd" />
|
||||
<option value="http://activemq.apache.org/schema/" />
|
||||
<option value="http://schema.cloudfoundry.org/spring/" />
|
||||
<option value="http://schemas.xmlsoap.org/" />
|
||||
<option value="http://cxf.apache.org/schemas/" />
|
||||
<option value="http://primefaces.org/ui" />
|
||||
<option value="http://tiles.apache.org/" />
|
||||
<option value="http://__IP__" />
|
||||
</list>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": true,
|
||||
"printWidth": 100
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 201 KiB |
@@ -1,33 +1,48 @@
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0) [](https://cpvalente.gitbook.io/ontime/)
|
||||
|
||||
|
||||
Download the latest releases here
|
||||
- [Windows](https://github.com/cpvalente/ontime/releases/download/v0.10-beta/ontime-win64.exe)
|
||||
<div style="display: flex; justify-content: space-around">
|
||||
<a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-macOS.dmg"><img alt="Download MacOS" src="https://github.com/cpvalente/ontime/blob/master/.github/mac-download.png"/></a>
|
||||
<a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-win64.exe"><img alt="Download Windows" src="https://github.com/cpvalente/ontime/blob/master/.github/win-download.png"/></a>
|
||||
<img alt="Download Linux" src="https://github.com/cpvalente/ontime/blob/master/.github/linux-download.png"/>
|
||||
</div>
|
||||
|
||||
# Ontime
|
||||
Ontime is an application for managing event rundowns and running stage timers.
|
||||
|
||||
It allows a center application to be able to distribute event information in the local network. This minimises needs for using Media Server outputs or expensive video distribution while allowing easy integration in workflows including OBS and d3.
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
## Using Ontime
|
||||
Once installed and running, ontime starts a background server that is the heart of all processes.
|
||||
The app, is used to add / edit your running order in the event list, and running the timers using the Playback Control function.
|
||||
|
||||
From here, any device in the same network with a browser is able to render the views as described. This is done by reaching the ontime server at the _default port 4001_ eg: `localhost:4001` or `192.168.1.3:4001`
|
||||
You can then use the the ontime logo on the top right corner to select the desired view.
|
||||
You can then use the ontime logo in the top right corner to select the desired view (event in the lower thirds view, where it is hidden).
|
||||
|
||||
In case of unnatended machines or automations, it is possible to use different URL to recall individual views
|
||||
In case of unattended machines or automations, it is possible to use different URL to recall individual views
|
||||
and extend with using the URL aliases feature
|
||||
|
||||
```
|
||||
IP.ADDRESS:4001 > Web server default to stage timer view
|
||||
IP.ADDRESS:4001/speaker > Speaker / Stage timer view
|
||||
IP.ADDRESS:4001/sm > Stage Manager / Backstage view
|
||||
IP.ADDRESS:4001/public > Public / Foyer view
|
||||
IP.ADDRESS:4001/pip > Picture in Picture view
|
||||
IP.ADDRESS:4001/lower > Lower Thirds
|
||||
For the presentation views...
|
||||
-------------------------------------------------------------
|
||||
IP.ADDRESS:4001 > Web server default to presenter timer view
|
||||
IP.ADDRESS:4001/timer > Presenter / Stage timer view
|
||||
IP.ADDRESS:4001/sm > Stage Manager / Backstage view
|
||||
IP.ADDRESS:4001/public > Public / Foyer view
|
||||
IP.ADDRESS:4001/pip > Picture in Picture view
|
||||
IP.ADDRESS:4001/lower > Lower Thirds
|
||||
IP.ADDRESS:4001/studio > Studio Clock
|
||||
IP.ADDRESS:4001/cuesheet > Cue Sheet
|
||||
|
||||
...and for the editor (the control interface, same as the app)
|
||||
-------------------------------------------------------------
|
||||
IP.ADDRESS:4001/studio > Studio Clock
|
||||
|
||||
```
|
||||
|
||||
More documentation available [here](https://cpvalente.gitbook.io/ontime/)
|
||||
@@ -38,46 +53,80 @@ More documentation available [here](https://cpvalente.gitbook.io/ontime/)
|
||||
- Backstage Info
|
||||
- Public Info
|
||||
- Picture in Picture
|
||||
- Studio Clock
|
||||
- [x] Configurable realtime Lower Thirds
|
||||
- [x] Cuesheets with additional custom fields
|
||||
- [x] Send live messages to different screen types
|
||||
- [x] Ability to differentiate between backstage and public data
|
||||
- [x] Manage delays workflow
|
||||
- [x] OSC Control and Feedback
|
||||
- [x] Roll mode: run independently from system clock
|
||||
- [x] Open Sound Control (OSC) Control and Feedback
|
||||
- [x] Roll mode: run independently using the system clock
|
||||
- [x] Import event list from Excel
|
||||
- [x] URL Aliases (define configurable aliases to ease onsite setup)
|
||||
- [x] Logging view
|
||||
- [x] Edit anywhere: run ontime in your local network and use any machine to reach the editor page (same as app)
|
||||
|
||||
## Unopinionated
|
||||
We are not interested in forcing workflows and have made ontime so it is flexible to whichever way you would like to work.
|
||||
|
||||
We are not interested in forcing workflows and have made ontime, so it is flexible to whichever way you would like to work.
|
||||
|
||||
- [x] You do not need an order list to use the timer. Create an empty event and the OSC API works just the same
|
||||
- [x] If you want just the info screens, no need to use the timer!
|
||||
- [x] Dont have or care for a schedule?
|
||||
- [x] Don't have or care for a schedule?
|
||||
- [x] a single event with no data is enough to use the OSC API and get going
|
||||
- [x] use the order list to create a set of quick timers by setting the beggining and start times to 00:00 and 00:10 (**BAM**! 10 minute timer). You can quick recall this with OSC as always
|
||||
- [x] use the order list to create a set of quick timers by setting the beginning and start times to 00:00 and 00:10 (**BAM**! 10 minute timer). You can quickly recall this with OSC as always
|
||||
|
||||
## Integrations and Workflow
|
||||
The app is being currently developed to a wide user base, from broadcast to entertainment and conference halls.
|
||||
|
||||
Taking advantage of the integrations in Ontime, we currently use Ontime with:
|
||||
- `disguise`: trigger ontime from d3's timeline using the **OSC API**, **render views** using d3's webmodule
|
||||
- `OBS`: **render views** using the Browser Module
|
||||
- `QLab`: trigger ontime using **OSC API**
|
||||
- `Companion`: trigger ontime and manipulate timer using **OSC API**
|
||||
|
||||
|
||||
## Roadmap
|
||||
### Continued development
|
||||
There are several features planned in the roadmap.
|
||||
These will be implemented in a development friendly order unless there is user demand to bump any of them.
|
||||
- [ ] HTTP Server (vMix integration)
|
||||
- [ ] Improvement with event component design
|
||||
- [ ] New playback mode for [cumulative time keeping](https://github.com/cpvalente/ontime/issues/100)
|
||||
- [ ] Linux version
|
||||
- [ ] Headless version (run server only anywhere, configure from a browser locally)
|
||||
- [ ] Companion module
|
||||
- [ ] Lower Third Manager
|
||||
- [ ] Note only event
|
||||
- [ ] Reach Schedule: way to speedup timer to meet a deadline
|
||||
|
||||
### For version 1
|
||||
Almost reaching a feature set that we can call v1. Before that:
|
||||
- [ ] Mac OS version
|
||||
- [ ] Finish Documentation
|
||||
### Continuing
|
||||
- [ ] Linux version
|
||||
- [ ] Headless version (run server only anywhere, configure from a browser locally)
|
||||
- [ ] Companion integration
|
||||
- [ ] Lower Third Manager
|
||||
- [ ] Note only event
|
||||
- [ ] URL Aliases (define configurable aliases to ease onsite setup)
|
||||
- [ ] Logging view
|
||||
- [ ] Reach Schedule: way to speedup timer to meet a deadline
|
||||
|
||||
### Issues
|
||||
The app is still in pre-release and there are a few issues, mainly concerning style.
|
||||
This will be receiving attention as we near v1 release
|
||||
The app is still in pre-release and there are a few issues, mainly concerning responsiveness in
|
||||
different screens. If you run into problems, please open an issue with a screenshot and your device
|
||||
resolution
|
||||
|
||||
- [ ] App appears visually broken: Please ensure that windows settings have no display zoom (it is 125% by default)
|
||||
- [ ] app is needs improvement on handling zoomed interfaces
|
||||
- [ ] Very long titles might cause interface to shift
|
||||
- [ ] App sometimes hides itself when window is not visible. Ontime is always running in the background, find it in the tray or press Alt+1 to bring it to the foreground
|
||||
#### Unsigned App
|
||||
|
||||
When installing the app you would see warning screens from the Operating System like:
|
||||
|
||||
```Microsoft Defender SmartScreen prevented an unrecognised app from starting. Running this app might put your PC at risk.```
|
||||
or
|
||||
```Ontime can't be opened because it is from an unidentified developer```
|
||||
Long story short: Ontime app is unsigned. </br>Purchasing the certificates for both Mac and Windows
|
||||
incurs an early cost and is not a priority. This is unlikely to change in future.
|
||||
If you have tips on how to improve this, or would like to sponsor the code signing,
|
||||
please [open an issue so we can discuss it](https://github.com/cpvalente/ontime/issues/new)
|
||||
|
||||
#### Safari
|
||||
|
||||
There are some issues with Safari versions lower than 13:
|
||||
- Spacing and text styles do not render correctly
|
||||
- Table view does not work
|
||||
|
||||
There is no plan for any further work on this since the breaking code belongs to third party libraries.
|
||||
|
||||
# Help
|
||||
Help is underway! ... and can be viewed [here](https://cpvalente.gitbook.io/ontime/)
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest",
|
||||
"plugin:react/recommended"
|
||||
],
|
||||
"plugins": [
|
||||
"react",
|
||||
"testing-library",
|
||||
"jest"
|
||||
],
|
||||
"rules": {
|
||||
"jest/no-mocks-import": "warn",
|
||||
"no-useless-concat": "warn",
|
||||
"prefer-template": "warn",
|
||||
"react/jsx-no-bind": [
|
||||
"error",
|
||||
{
|
||||
"ignoreRefs": true,
|
||||
"allowArrowFunctions": true,
|
||||
"allowFunctions": false,
|
||||
"allowBind": false,
|
||||
"ignoreDOMComponents": true
|
||||
}
|
||||
],
|
||||
"react/jsx-boolean-value": [
|
||||
"error",
|
||||
"never",
|
||||
{
|
||||
"always": []
|
||||
}
|
||||
],
|
||||
"react/jsx-handler-names": [
|
||||
"off",
|
||||
{
|
||||
"eventHandlerPrefix": "handle",
|
||||
"eventHandlerPropPrefix": "on"
|
||||
}
|
||||
],
|
||||
"react/no-danger": "warn",
|
||||
"react/no-deprecated": [
|
||||
"error"
|
||||
],
|
||||
"react/jsx-no-undef": "error",
|
||||
"react/jsx-closing-tag-location": "warn",
|
||||
"react/jsx-no-useless-fragment": "warn",
|
||||
"react/no-unescaped-entities": "error",
|
||||
"react/jsx-tag-spacing": [
|
||||
"error",
|
||||
{
|
||||
"closingSlash": "never",
|
||||
"beforeSelfClosing": "always",
|
||||
"afterOpening": "never",
|
||||
"beforeClosing": "never"
|
||||
}
|
||||
],
|
||||
"react/jsx-space-before-closing": [
|
||||
"off",
|
||||
"always"
|
||||
],
|
||||
"react/jsx-curly-brace-presence": [
|
||||
"error",
|
||||
{
|
||||
"props": "never",
|
||||
"children": "never"
|
||||
}
|
||||
],
|
||||
"react/destructuring-assignment": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"react/no-unsafe": "off",
|
||||
"react/prop-types": [
|
||||
"error",
|
||||
{
|
||||
"ignore": [],
|
||||
"customValidators": [],
|
||||
"skipUndeclared": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+31
-27
@@ -3,42 +3,37 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@chakra-ui/react": "^1.6.10",
|
||||
"@emotion/react": "^11.5.0",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"axios": "^0.21.1",
|
||||
"framer-motion": "^4.1.6",
|
||||
"jotai": "^0.16.5",
|
||||
"react": "^17.0.1",
|
||||
"@chakra-ui/react": "^1.8.6",
|
||||
"@dnd-kit/core": "^5.0.3",
|
||||
"@dnd-kit/sortable": "^6.0.1",
|
||||
"@dnd-kit/utilities": "^3.1.0",
|
||||
"@emotion/react": "^11.7.1",
|
||||
"@emotion/styled": "^11.6.0",
|
||||
"@react-icons/all-files": "^4.1.0",
|
||||
"autosize": "^5.0.1",
|
||||
"axios": "^0.25.0",
|
||||
"color": "^4.2.1",
|
||||
"framer-motion": "4.1.17",
|
||||
"react": "17.0.2",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-fast-compare": "^3.2.0",
|
||||
"react-icons": "^4.3.1",
|
||||
"react-qr-code": "^2.0.2",
|
||||
"react-query": "^3.13.5",
|
||||
"react-router": "^5.2.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "4.0.3",
|
||||
"socket.io-client": "^4.3.2",
|
||||
"tinykeys": "^1.1.2",
|
||||
"react-qr-code": "2.0.3",
|
||||
"react-query": "^3.34.12",
|
||||
"react-router-dom": "^6.2.1",
|
||||
"react-table": "^7.7.0",
|
||||
"socket.io-client": "^4.4.1",
|
||||
"typeface-open-sans": "^1.1.13",
|
||||
"web-vitals": "^1.0.1"
|
||||
"use-fit-text": "^2.4.0",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"start": "set BROWSER=none&&react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"test:pipeline": "react-scripts test --watchAll=false --runInBand --detectOpenHandles --forceExit ",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
@@ -51,5 +46,14 @@
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {}
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^12.1.2",
|
||||
"@testing-library/react-hooks": "^7.0.2",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-scripts": "^5.0.0",
|
||||
"react-test-renderer": "^17.0.2",
|
||||
"sass": "^1.49.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
* {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body,
|
||||
html,
|
||||
.App {
|
||||
margin: 0px auto;
|
||||
overflow: hidden;
|
||||
overflow: clip;
|
||||
height: 100vh;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
option {
|
||||
color: black;
|
||||
}
|
||||
+103
-58
@@ -1,81 +1,126 @@
|
||||
import { lazy, Suspense, useEffect } from 'react';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import './App.css';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import SocketProvider from 'app/context/socketContext';
|
||||
import React, { lazy, Suspense, useCallback, useEffect } from 'react';
|
||||
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
||||
import './App.scss';
|
||||
import withSocket from 'features/viewers/ViewWrapper';
|
||||
import tinykeys from 'tinykeys';
|
||||
import ErrorBoundary from 'common/components/errorBoundary/ErrorBoundary';
|
||||
import ProtectRoute from './common/components/protectRoute/ProtectRoute';
|
||||
import { useFetch } from './app/hooks/useFetch';
|
||||
import { ALIASES } from './app/api/apiConstants';
|
||||
import { getAliases } from './app/api/ontimeApi';
|
||||
import { TableSettingsProvider } from './app/context/TableSettingsContext';
|
||||
|
||||
const Editor = lazy(() => import('features/editors/Editor'));
|
||||
const PresenterView = lazy(() =>
|
||||
import('features/viewers/presenter/PresenterView')
|
||||
);
|
||||
const PresenterSimple = lazy(() =>
|
||||
import('features/viewers/presenter/PresenterSimple')
|
||||
);
|
||||
const StageManager = lazy(() =>
|
||||
import('features/viewers/backstage/StageManager')
|
||||
);
|
||||
const Table = lazy(() => import('features/table/TableWrapper'));
|
||||
|
||||
const TimerView = lazy(() => import('features/viewers/timer/Timer'));
|
||||
const MinimalTimerView = lazy(() => import('features/viewers/timer/MinimalTimer'));
|
||||
|
||||
const StageManager = lazy(() => import('features/viewers/backstage/StageManager'));
|
||||
const Public = lazy(() => import('features/viewers/foh/Public'));
|
||||
const Lower = lazy(() =>
|
||||
import('features/viewers/production/lower/LowerWrapper')
|
||||
);
|
||||
const Lower = lazy(() => import('features/viewers/production/lower/LowerWrapper'));
|
||||
const Pip = lazy(() => import('features/viewers/production/Pip'));
|
||||
const StudioClock = lazy(() => import('features/viewers/studio/StudioClock'));
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
// Seemed to cause issues
|
||||
// broadcastQueryClient({
|
||||
// queryClient,
|
||||
// broadcastChannel: 'ontime',
|
||||
// });
|
||||
|
||||
const SSpeaker = withSocket(PresenterView);
|
||||
const SSpeakerSimple = withSocket(PresenterSimple);
|
||||
const STimer = withSocket(TimerView);
|
||||
const SMinimalTimer = withSocket(MinimalTimerView);
|
||||
const SStageManager = withSocket(StageManager);
|
||||
const SPublic = withSocket(Public);
|
||||
const SLowerThird = withSocket(Lower);
|
||||
const SPip = withSocket(Pip);
|
||||
const SStudio = withSocket(StudioClock);
|
||||
|
||||
const ProtectedEditor = () => (
|
||||
<ProtectRoute>
|
||||
<Editor />
|
||||
</ProtectRoute>
|
||||
);
|
||||
|
||||
const ProtectedTable = () => (
|
||||
<ProtectRoute>
|
||||
<TableSettingsProvider>
|
||||
<Table />
|
||||
</TableSettingsProvider>
|
||||
</ProtectRoute>
|
||||
);
|
||||
|
||||
function App() {
|
||||
useEffect(() => {
|
||||
let unsubscribe = tinykeys(window, {
|
||||
'Alt+t': () => {
|
||||
const { data } = useFetch(ALIASES, getAliases);
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
const handleKeyPress = useCallback((e) => {
|
||||
// handle held key
|
||||
if (e.repeat) return;
|
||||
// check if the alt key is pressed
|
||||
if (e.altKey) {
|
||||
if (e.key === 't' || e.key === 'T') {
|
||||
// if we are in electron
|
||||
if (window.process?.type === undefined) return;
|
||||
if (window.process.type === 'renderer') {
|
||||
if (window.process?.type === 'renderer') {
|
||||
// ask to see debug
|
||||
window.ipcRenderer.send('set-window', 'show-dev');
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// attach the event listener
|
||||
document.addEventListener('keydown', handleKeyPress);
|
||||
|
||||
// remove the event listener
|
||||
return () => {
|
||||
unsubscribe();
|
||||
document.removeEventListener('keydown', handleKeyPress);
|
||||
};
|
||||
});
|
||||
}, [handleKeyPress]);
|
||||
|
||||
// navigate if is alias route
|
||||
useEffect(() => {
|
||||
if (data == null) return;
|
||||
for (const d of data) {
|
||||
if (`/${d.alias}` === location.pathname && d.enabled) {
|
||||
navigate(`/${d.pathAndParams}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [data, location, navigate]);
|
||||
|
||||
return (
|
||||
<SocketProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<div className='App'>
|
||||
<Suspense fallback={null}>
|
||||
<Switch>
|
||||
<Route exact path='/' component={SSpeaker} />
|
||||
<Route exact path='/sm' component={SStageManager} />
|
||||
<Route exact path='/speaker' component={SSpeaker} />
|
||||
<Route exact path='/stage' component={SSpeaker} />
|
||||
<Route exact path='/speakersimple' component={SSpeakerSimple} />
|
||||
<Route exact path='/editor' component={Editor} />
|
||||
<Route exact path='/public' component={SPublic} />
|
||||
<Route exact path='/pip' component={SPip} />
|
||||
{/* Lower cannot have fallback */}
|
||||
<Route exact path='/lower' component={SLowerThird} />
|
||||
{/* Send to default if nothing found */}
|
||||
<Route component={SSpeaker} />
|
||||
</Switch>
|
||||
</Suspense>
|
||||
</div>
|
||||
</QueryClientProvider>
|
||||
</SocketProvider>
|
||||
<div className='App'>
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback={null}>
|
||||
<Routes>
|
||||
<Route path='/' element={<STimer />} />
|
||||
<Route path='/speaker' element={<STimer />} />
|
||||
<Route path='/presenter' element={<STimer />} />
|
||||
<Route path='/stage' element={<STimer />} />
|
||||
<Route path='/timer' element={<STimer />} />
|
||||
|
||||
<Route path='/minimal' element={<SMinimalTimer />} />
|
||||
<Route path='/minimalTimer' element={<SMinimalTimer />} />
|
||||
<Route path='/simpleTimer' element={<SMinimalTimer />} />
|
||||
|
||||
<Route path='/sm' element={<SStageManager />} />
|
||||
<Route path='/backstage' element={<SStageManager />} />
|
||||
|
||||
<Route path='/public' element={<SPublic />} />
|
||||
<Route path='/pip' element={<SPip />} />
|
||||
<Route path='/studio' element={<SStudio />} />
|
||||
{/*/!* Lower cannot have fallback *!/*/}
|
||||
<Route path='/lower' element={<SLowerThird />} />
|
||||
|
||||
{/*/!* Protected Routes *!/*/}
|
||||
<Route path='/editor' element={<ProtectedEditor />} />
|
||||
<Route path='/cuesheet' element={<ProtectedTable />} />
|
||||
<Route path='/cuelist' element={<ProtectedTable />} />
|
||||
<Route path='/table' element={<ProtectedTable />} />
|
||||
|
||||
{/* Send to default if nothing found */}
|
||||
<Route path='*' element={<STimer />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
* {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
scrollbar-color: rgba(255, 255, 255, 0.15) rgba(255, 255, 255, 0.01);
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
body,
|
||||
html,
|
||||
.App {
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
overflow: clip;
|
||||
height: 100vh;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
option {
|
||||
color: black;
|
||||
}
|
||||
|
||||
/* width */
|
||||
::-webkit-scrollbar {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.01);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Handle on hover */
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import {QueryClient} from "react-query";
|
||||
|
||||
export const queryClientMock = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,7 +1,11 @@
|
||||
export const NODE_PORT = 4001;
|
||||
export const EVENT_TABLE = 'event';
|
||||
export const ALIASES = 'aliases';
|
||||
export const USERFIELDS = 'userFields';
|
||||
export const EVENTS_TABLE = 'events';
|
||||
export const APP_TABLE = 'appinfo';
|
||||
export const OSC_SETTINGS = 'oscSettings';
|
||||
export const APP_SETTINGS = 'appSettings';
|
||||
|
||||
const calculateServer = () => {
|
||||
return window.location.origin.replace(window.location.port, `${NODE_PORT}/`);
|
||||
@@ -10,5 +14,5 @@ const calculateServer = () => {
|
||||
export const serverURL = calculateServer();
|
||||
export const eventURL = serverURL + EVENT_TABLE;
|
||||
export const eventsURL = serverURL + EVENTS_TABLE;
|
||||
export const playbackURL = serverURL + 'playback';
|
||||
export const ontimeURL = serverURL + 'ontime';
|
||||
export const playbackURL = `${serverURL}playback`;
|
||||
export const ontimeURL = `${serverURL}ontime`;
|
||||
|
||||
@@ -6,7 +6,4 @@ export const fetchEvent = async () => {
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const postEvent = async (data) => {
|
||||
const res = await axios.post(eventURL, data);
|
||||
return res;
|
||||
};
|
||||
export const postEvent = async (data) => axios.post(eventURL, data);
|
||||
|
||||
@@ -1,44 +1,21 @@
|
||||
import axios from 'axios';
|
||||
import { eventsURL } from '../api/apiConstants';
|
||||
import { eventsURL } from './apiConstants';
|
||||
|
||||
export const fetchAllEvents = async () => {
|
||||
const res = await axios.get(eventsURL);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const requestPost = async (data) => {
|
||||
const res = await axios.post(eventsURL, data);
|
||||
return res;
|
||||
};
|
||||
export const requestPost = async (data) => axios.post(eventsURL, data);
|
||||
|
||||
export const requestPut = async (data) => {
|
||||
const res = await axios.put(eventsURL, data);
|
||||
return res;
|
||||
};
|
||||
export const requestPut = async (data) => axios.put(eventsURL, data);
|
||||
|
||||
export const requestPatch = async (data) => {
|
||||
const res = await axios.patch(eventsURL, data);
|
||||
return res;
|
||||
};
|
||||
export const requestPatch = async (data) => axios.patch(eventsURL, data);
|
||||
|
||||
export const requestReorder = async (data) => {
|
||||
const action = 'reorder';
|
||||
const res = await axios.patch(eventsURL + '/' + action, data);
|
||||
return res;
|
||||
};
|
||||
export const requestReorder = async (data) => axios.patch(`${eventsURL}/reorder`, data);
|
||||
|
||||
export const requestApplyDelay = async (eventId) => {
|
||||
const action = 'applydelay';
|
||||
const res = await axios.patch(eventsURL + '/' + action + '/' + eventId);
|
||||
return res;
|
||||
};
|
||||
export const requestApplyDelay = async (eventId) => axios.patch(`${eventsURL}/applydelay/${eventId}`);
|
||||
|
||||
export const requestDelete = async (eventId) => {
|
||||
const res = await axios.delete(eventsURL + '/' + eventId);
|
||||
return res;
|
||||
};
|
||||
export const requestDelete = async (eventId) => axios.delete(`${eventsURL}/${eventId}`);
|
||||
|
||||
export const requestDeleteAll = async () => {
|
||||
const res = await axios.delete(eventsURL + '/all');
|
||||
return res;
|
||||
};
|
||||
export const requestDeleteAll = async () => axios.delete(`${eventsURL}/all`);
|
||||
|
||||
+135
-26
@@ -3,36 +3,150 @@ import { ontimeURL } from './apiConstants';
|
||||
|
||||
export const ontimePlaceholderInfo = {
|
||||
networkInterfaces: [],
|
||||
version: '',
|
||||
serverPort: 4001,
|
||||
oscInPort: '',
|
||||
oscOutPort: '',
|
||||
oscOutIP: '',
|
||||
settings: {
|
||||
version: '',
|
||||
serverPort: 4001,
|
||||
},
|
||||
};
|
||||
|
||||
export const getInfo = async () => {
|
||||
const res = await axios.get(ontimeURL + '/info');
|
||||
export const ontimePlaceholderSettings = {
|
||||
pinCode: null,
|
||||
};
|
||||
|
||||
export const eventPlaceholderSettings = {
|
||||
title: '',
|
||||
url: '',
|
||||
publicInfo: '',
|
||||
backstageInfo: '',
|
||||
endMessage: '',
|
||||
};
|
||||
|
||||
export const userFieldsPlaceholder = {
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
user3: '',
|
||||
user4: '',
|
||||
user5: '',
|
||||
user6: '',
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
};
|
||||
|
||||
export const oscPlaceholderSettings = {
|
||||
port: '',
|
||||
portOut: '',
|
||||
targetIP: '',
|
||||
enabled: false,
|
||||
};
|
||||
|
||||
export const httpPlaceholder = {
|
||||
onLoad: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
onStart: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
onUpdate: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
onPause: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
onStop: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
onFinish: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const ontimeVars = [
|
||||
{
|
||||
name: '$timer',
|
||||
description: 'Current running timer',
|
||||
},
|
||||
{
|
||||
name: '$title',
|
||||
description: 'Current title',
|
||||
},
|
||||
{
|
||||
name: '$presenter',
|
||||
description: 'Current timer',
|
||||
},
|
||||
{
|
||||
name: '$subtitle',
|
||||
description: 'Current subtitle',
|
||||
},
|
||||
{
|
||||
name: '$next-title',
|
||||
description: 'Next title',
|
||||
},
|
||||
{
|
||||
name: '$next-presenter',
|
||||
description: 'Next timer',
|
||||
},
|
||||
{
|
||||
name: '$next-subtitle',
|
||||
description: 'Next subtitle',
|
||||
},
|
||||
];
|
||||
|
||||
export const getSettings = async () => {
|
||||
const res = await axios.get(`${ontimeURL}/settings`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const postInfo = async (data) => {
|
||||
const res = await axios.post(ontimeURL + '/info', data);
|
||||
return res;
|
||||
export const postSettings = async (data) => axios.post(`${ontimeURL}/settings`, data);
|
||||
|
||||
export const getInfo = async () => {
|
||||
const res = await axios.get(`${ontimeURL}/info`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const postInfo = async (data) => axios.post(`${ontimeURL}/info`, data);
|
||||
|
||||
export const getAliases = async () => {
|
||||
const res = await axios.get(`${ontimeURL}/aliases`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const postAliases = async (data) => axios.post(`${ontimeURL}/aliases`, data);
|
||||
|
||||
export const getUserFields = async () => {
|
||||
const res = await axios.get(`${ontimeURL}/userfields`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const postUserFields = async (data) => axios.post(`${ontimeURL}/userfields`, data);
|
||||
|
||||
export const getOSC = async () => {
|
||||
const res = await axios.get(`${ontimeURL}/osc`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const postOSC = async (data) => axios.post(`${ontimeURL}/osc`, data);
|
||||
|
||||
export const downloadEvents = async () => {
|
||||
await axios({
|
||||
url: ontimeURL + '/db',
|
||||
url: `${ontimeURL}/db`,
|
||||
method: 'GET',
|
||||
responseType: 'blob', // important
|
||||
}).then((response) => {
|
||||
let headerLine = response.headers['Content-Disposition'];
|
||||
const headerLine = response.headers['Content-Disposition'];
|
||||
let filename = 'events.json';
|
||||
|
||||
// try and get the filename from the response
|
||||
if (headerLine != null) {
|
||||
let startFileNameIndex = headerLine.indexOf('"') + 1;
|
||||
let endFileNameIndex = headerLine.lastIndexOf('"');
|
||||
const startFileNameIndex = headerLine.indexOf('"') + 1;
|
||||
const endFileNameIndex = headerLine.lastIndexOf('"');
|
||||
filename = headerLine.substring(startFileNameIndex, endFileNameIndex);
|
||||
}
|
||||
|
||||
@@ -47,17 +161,12 @@ export const downloadEvents = async () => {
|
||||
|
||||
export const uploadEvents = async (file) => {
|
||||
const formData = new FormData();
|
||||
formData.append('jsondb', file); // appending file
|
||||
await axios
|
||||
.post(ontimeURL + '/db', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})
|
||||
.then((res) => console.log(res.data))
|
||||
.catch((err) => console.error(err));
|
||||
formData.append('userFile', file); // appending file
|
||||
await axios.post(`${ontimeURL}/db`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const uploadEventsWithPath = async (filepath) => {
|
||||
await axios.post(ontimeURL + '/dbpath', { path: filepath });
|
||||
};
|
||||
export const uploadEventsWithPath = async (filepath) => axios.post(`${ontimeURL}/dbpath`, { path: filepath });
|
||||
|
||||
@@ -1,37 +1,16 @@
|
||||
import axios from 'axios';
|
||||
import { playbackURL } from '../api/apiConstants';
|
||||
import { playbackURL } from './apiConstants';
|
||||
|
||||
export const getStart = async () => {
|
||||
const res = await axios.get(playbackURL + '/start');
|
||||
return res;
|
||||
};
|
||||
export const getStart = async () => axios.get(`${playbackURL}/start`);
|
||||
|
||||
export const getPause = async () => {
|
||||
const res = axios.get(playbackURL + '/pause');
|
||||
return res;
|
||||
};
|
||||
export const getPause = async () => axios.get(`${playbackURL}/pause`);
|
||||
|
||||
export const getRoll = async () => {
|
||||
const res = axios.get(playbackURL + '/roll');
|
||||
return res;
|
||||
};
|
||||
export const getRoll = async () => axios.get(`${playbackURL}/roll`);
|
||||
|
||||
export const getPrevious = async () => {
|
||||
const res = axios.get(playbackURL + '/previous');
|
||||
return res;
|
||||
};
|
||||
export const getPrevious = async () => axios.get(`${playbackURL}/previous`);
|
||||
|
||||
export const getNext = async () => {
|
||||
const res = axios.get(playbackURL + '/next');
|
||||
return res;
|
||||
};
|
||||
export const getNext = async () => axios.get(`${playbackURL}/next`);
|
||||
|
||||
export const getUnload = async () => {
|
||||
const res = axios.get(playbackURL + '/unload');
|
||||
return res;
|
||||
};
|
||||
export const getUnload = async () => axios.get(`${playbackURL}/unload`);
|
||||
|
||||
export const getReload = async () => {
|
||||
const res = axios.get(playbackURL + '/reload');
|
||||
return res;
|
||||
};
|
||||
export const getReload = async () => axios.get(`${playbackURL}/reload`);
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
// Exported viewer link location
|
||||
const speakerLocation = 'speaker';
|
||||
const smLocation = 'sm';
|
||||
const publicLocation = 'public';
|
||||
const pipLocation = 'pip';
|
||||
const studioLocation = 'studio';
|
||||
const cuesheetLocation = 'cuesheet';
|
||||
|
||||
export const viewerLocations = [
|
||||
{ link: speakerLocation, label: 'Speaker Screen' },
|
||||
{ link: smLocation, label: 'Backstage Screen' },
|
||||
{ link: publicLocation, label: 'Public Screen' },
|
||||
{ link: pipLocation, label: 'Picture in Picture' },
|
||||
{ link: studioLocation, label: 'Studio Clock' },
|
||||
{ link: cuesheetLocation, label: 'Cuesheet' }
|
||||
];
|
||||
@@ -0,0 +1,36 @@
|
||||
import React, { createContext, useCallback, useEffect, useState } from 'react';
|
||||
import { useFetch } from '../hooks/useFetch';
|
||||
import { APP_SETTINGS } from '../api/apiConstants';
|
||||
import { getSettings } from '../api/ontimeApi';
|
||||
|
||||
export const AppContext = createContext({
|
||||
auth: false,
|
||||
data: {
|
||||
pinCode: null,
|
||||
},
|
||||
});
|
||||
|
||||
export const AppContextProvider = ({ children }) => {
|
||||
const [auth, setAuth] = useState(true);
|
||||
const { data } = useFetch(APP_SETTINGS, getSettings);
|
||||
|
||||
useEffect(() => {
|
||||
if (data == null) return;
|
||||
if (data?.pinCode === null || data?.pinCode === '') {
|
||||
setAuth(true);
|
||||
} else {
|
||||
setAuth(false);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const validate = useCallback(
|
||||
(pin) => {
|
||||
const correct = pin === data.pinCode;
|
||||
setAuth(correct);
|
||||
return correct;
|
||||
},
|
||||
[data]
|
||||
);
|
||||
|
||||
return <AppContext.Provider value={{ auth, validate }}>{children}</AppContext.Provider>;
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import React, { createContext, useCallback } from 'react';
|
||||
import { useLocalStorage } from '../hooks/useLocalStorage';
|
||||
|
||||
export const CollapseContext = createContext({
|
||||
collapsed: {},
|
||||
setCollapsed: () => undefined,
|
||||
clearCollapsed: () => undefined,
|
||||
isCollapsed: () => undefined,
|
||||
});
|
||||
|
||||
export const CollapseProvider = ({ children }) => {
|
||||
const [collapsed, saveCollapsed] = useLocalStorage('collapsed', {});
|
||||
|
||||
/**
|
||||
* @description Sets collapsed state for a single id
|
||||
* @param {string} - id
|
||||
* @param {boolean} - collapsed / not collapsed
|
||||
*/
|
||||
const setCollapsed = useCallback(
|
||||
(id, isCollapsed) => {
|
||||
if (isCollapsed) {
|
||||
saveCollapsed((prev) => ({ ...prev, [id]: true }));
|
||||
} else {
|
||||
saveCollapsed((prev) => {
|
||||
const newObject = { ...prev };
|
||||
delete newObject[id];
|
||||
return { ...newObject };
|
||||
});
|
||||
}
|
||||
},
|
||||
[saveCollapsed]
|
||||
);
|
||||
|
||||
/**
|
||||
* @description Clears collapsed state in local-storage
|
||||
*/
|
||||
const collapseMultiple = useCallback(
|
||||
(events) => {
|
||||
const newOptions = {};
|
||||
for (const event of events) {
|
||||
newOptions[event.id] = true;
|
||||
}
|
||||
saveCollapsed(newOptions);
|
||||
},
|
||||
[saveCollapsed]
|
||||
);
|
||||
|
||||
/**
|
||||
* @description Clears collapsed state in local-storage
|
||||
*/
|
||||
const expandAll = useCallback(() => {
|
||||
saveCollapsed({});
|
||||
}, [saveCollapsed]);
|
||||
|
||||
/**
|
||||
* @description Clears collapsed state in local-storage
|
||||
* @return {boolean} - collapsed / not collapsed
|
||||
*/
|
||||
const isCollapsed = useCallback(
|
||||
(id) => {
|
||||
return id in collapsed;
|
||||
},
|
||||
[collapsed]
|
||||
);
|
||||
|
||||
return (
|
||||
<CollapseContext.Provider value={{ setCollapsed, collapseMultiple, expandAll, isCollapsed }}>
|
||||
{children}
|
||||
</CollapseContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import React, { createContext, useCallback, useMemo, useState } from 'react';
|
||||
import { useLocalStorage } from '../hooks/useLocalStorage';
|
||||
|
||||
export const CursorContext = createContext({
|
||||
cursor: 0,
|
||||
isCursorLocked: false,
|
||||
|
||||
setCursor: () => undefined,
|
||||
moveCursorUp: () => undefined,
|
||||
moveCursorDown: () => undefined,
|
||||
});
|
||||
|
||||
export const CursorProvider = ({ children }) => {
|
||||
const [cursor, setCursor] = useState(0);
|
||||
const [_cursorLocked, _setCursorLocked] = useLocalStorage('isCursorLocked', 'locked');
|
||||
const isCursorLocked = useMemo(() => _cursorLocked === 'locked', [_cursorLocked]);
|
||||
|
||||
const cursorLockedOff = useCallback(() => _setCursorLocked('unlocked'), [_setCursorLocked]);
|
||||
const cursorLockedOn = useCallback(() => _setCursorLocked('locked'), [_setCursorLocked]);
|
||||
|
||||
const moveCursorUp = useCallback(() => {
|
||||
setCursor((prev) => Math.max(prev - 1, 0));
|
||||
}, []);
|
||||
|
||||
const moveCursorDown = useCallback(() => {
|
||||
setCursor((prev) => prev + 1);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* @param {boolean | undefined} newValue
|
||||
*/
|
||||
const toggleCursorLocked = useCallback(
|
||||
(newValue = undefined) => {
|
||||
if (typeof newValue === 'undefined') {
|
||||
if (isCursorLocked) {
|
||||
cursorLockedOff();
|
||||
} else {
|
||||
cursorLockedOn();
|
||||
}
|
||||
} else if (!newValue) {
|
||||
cursorLockedOff();
|
||||
} else if (newValue) {
|
||||
cursorLockedOn();
|
||||
}
|
||||
},
|
||||
[cursorLockedOff, cursorLockedOn, isCursorLocked]
|
||||
);
|
||||
|
||||
return (
|
||||
<CursorContext.Provider
|
||||
value={{
|
||||
cursor,
|
||||
isCursorLocked,
|
||||
toggleCursorLocked,
|
||||
setCursor,
|
||||
moveCursorUp,
|
||||
moveCursorDown,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</CursorContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import React, { createContext, useState } from 'react';
|
||||
|
||||
export const LocalEventSettingsContext = createContext({
|
||||
showQuickEntry: false,
|
||||
starTimeIsLastEnd: true,
|
||||
defaultPublic: true,
|
||||
|
||||
setShowQuickEntry: () => undefined,
|
||||
setStarTimeIsLastEnd: () => undefined,
|
||||
setDefaultPublic: () => undefined,
|
||||
});
|
||||
|
||||
export const LocalEventSettingsProvider = ({ children }) => {
|
||||
const [showQuickEntry, setShowQuickEntry] = useState(false);
|
||||
const [starTimeIsLastEnd, setStarTimeIsLastEnd] = useState(true);
|
||||
const [defaultPublic, setDefaultPublic] = useState(false);
|
||||
|
||||
return (
|
||||
<LocalEventSettingsContext.Provider
|
||||
value={{
|
||||
showQuickEntry,
|
||||
setShowQuickEntry,
|
||||
starTimeIsLastEnd,
|
||||
setStarTimeIsLastEnd,
|
||||
defaultPublic,
|
||||
setDefaultPublic,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</LocalEventSettingsContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
import React, { createContext, useCallback, useEffect, useState } from 'react';
|
||||
import { useSocket } from './socketContext';
|
||||
import { generateId } from '../../common/utils/generate_id';
|
||||
import { nowInMillis, stringFromMillis } from '../../common/utils/time';
|
||||
|
||||
export const LoggingContext = createContext({
|
||||
logData: [],
|
||||
emitInfo: () => undefined,
|
||||
emitWarning: () => undefined,
|
||||
emitError: () => undefined,
|
||||
clearLog: () => undefined,
|
||||
});
|
||||
|
||||
export const LoggingProvider = ({ children }) => {
|
||||
const MAX_MESSAGES = 100;
|
||||
const socket = useSocket();
|
||||
const [logData, setLogData] = useState([]);
|
||||
const origin = 'USER';
|
||||
|
||||
// handle incoming messages
|
||||
useEffect(() => {
|
||||
if (socket == null) return;
|
||||
|
||||
// Ask for log data
|
||||
socket.emit('get-logger');
|
||||
|
||||
socket.on('logger', (data) => {
|
||||
setLogData((l) => [data, ...l]);
|
||||
});
|
||||
|
||||
// Clear listener
|
||||
return () => {
|
||||
socket.off('logger');
|
||||
};
|
||||
}, [socket]);
|
||||
|
||||
/**
|
||||
* Utility function sends message over socket
|
||||
* @param text
|
||||
* @param level
|
||||
* @private
|
||||
*/
|
||||
const _send = useCallback(
|
||||
(text, level) => {
|
||||
if (socket != null) {
|
||||
const m = {
|
||||
id: generateId(),
|
||||
origin,
|
||||
time: stringFromMillis(nowInMillis()),
|
||||
level,
|
||||
text,
|
||||
};
|
||||
setLogData((l) => [m, ...l]);
|
||||
socket.emit('logger', m);
|
||||
}
|
||||
if (logData.length > MAX_MESSAGES) {
|
||||
setLogData((l) => l.pop());
|
||||
}
|
||||
},
|
||||
[logData, socket]
|
||||
);
|
||||
|
||||
/**
|
||||
* Sends a message with level INFO
|
||||
* @param text
|
||||
*/
|
||||
const emitInfo = useCallback(
|
||||
(text) => {
|
||||
_send(text, 'INFO');
|
||||
},
|
||||
[_send]
|
||||
);
|
||||
|
||||
/**
|
||||
* Sends a message with level WARN
|
||||
* @param text
|
||||
*/
|
||||
const emitWarning = useCallback(
|
||||
(text) => {
|
||||
_send(text, 'WARN');
|
||||
},
|
||||
[_send]
|
||||
);
|
||||
|
||||
/**
|
||||
* Sends a message with level ERROR
|
||||
* @param text
|
||||
*/
|
||||
const emitError = useCallback(
|
||||
(text) => {
|
||||
_send(text, 'ERROR');
|
||||
},
|
||||
[_send]
|
||||
);
|
||||
|
||||
/**
|
||||
* Clears running log
|
||||
*/
|
||||
const clearLog = useCallback(() => {
|
||||
setLogData([]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<LoggingContext.Provider value={{ emitInfo, logData, emitWarning, emitError, clearLog }}>
|
||||
{children}
|
||||
</LoggingContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
import React, { createContext, useCallback, useState } from 'react';
|
||||
import { useLocalStorage } from '../hooks/useLocalStorage';
|
||||
|
||||
export const TableSettingsContext = createContext({
|
||||
theme: '',
|
||||
showSettings: false,
|
||||
followSelected: false,
|
||||
|
||||
toggleSettings: () => undefined,
|
||||
toggleTheme: () => undefined,
|
||||
toggleFollow: () => undefined,
|
||||
});
|
||||
|
||||
export const TableSettingsProvider = ({ children }) => {
|
||||
const [theme, setTheme] = useLocalStorage('table-color-theme', 'dark');
|
||||
const [followSelected, setFollowSelected] = useLocalStorage('table-follow-selected', false);
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
|
||||
/**
|
||||
* @description Toggles the current value of dark mode
|
||||
* @param {string} val - 'light' or 'dark'
|
||||
*/
|
||||
const toggleTheme = useCallback(
|
||||
(val) => {
|
||||
if (val === undefined) {
|
||||
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
|
||||
} else {
|
||||
setTheme(val);
|
||||
}
|
||||
},
|
||||
[setTheme]
|
||||
);
|
||||
|
||||
/**
|
||||
* @description Toggles visibility state for settings
|
||||
* @param {boolean} val - whether the settings window is visible
|
||||
*/
|
||||
const toggleSettings = useCallback(
|
||||
(val) => {
|
||||
if (val === undefined) {
|
||||
setShowSettings((prev) => !prev);
|
||||
} else {
|
||||
setShowSettings(val);
|
||||
}
|
||||
},
|
||||
[setShowSettings]
|
||||
);
|
||||
|
||||
/**
|
||||
* @description Toggles follow option
|
||||
* @param {boolean} val - whether the window follows selected event
|
||||
*/
|
||||
const toggleFollow = useCallback(
|
||||
(val) => {
|
||||
if (val === undefined) {
|
||||
setFollowSelected((prev) => !prev);
|
||||
} else {
|
||||
setFollowSelected(val);
|
||||
}
|
||||
},
|
||||
[setFollowSelected]
|
||||
);
|
||||
|
||||
return (
|
||||
<TableSettingsContext.Provider
|
||||
value={{
|
||||
theme,
|
||||
showSettings,
|
||||
followSelected,
|
||||
toggleSettings,
|
||||
toggleTheme,
|
||||
toggleFollow,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TableSettingsContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
const { atom } = require('jotai');
|
||||
|
||||
const PATH = 'option-collapse';
|
||||
const initialValue = {};
|
||||
|
||||
// collapse options object, initialised from local storage
|
||||
// REFRACT: When do we read from local storage?
|
||||
// on every render?
|
||||
|
||||
export const collapseAtom = atom(
|
||||
(get) => {
|
||||
const storedOptions = localStorage.getItem(PATH);
|
||||
if (storedOptions == null) return initialValue;
|
||||
|
||||
return JSON.parse(storedOptions);
|
||||
},
|
||||
(get, set, newValues) => {
|
||||
set(collapseAtom, newValues);
|
||||
localStorage.setItem(PATH, JSON.stringify(newValues));
|
||||
}
|
||||
);
|
||||
|
||||
// get a single option, if it exists
|
||||
export const SelectCollapse = (id) => {
|
||||
return atom((get) => get(collapseAtom)[id]);
|
||||
};
|
||||
|
||||
// change a single item in object
|
||||
export const HandleCollapse = atom(null, (get, set, payload) => {
|
||||
const updatedVal = {
|
||||
...get(collapseAtom),
|
||||
...payload,
|
||||
};
|
||||
set(collapseAtom, updatedVal);
|
||||
localStorage.setItem(PATH, JSON.stringify(updatedVal));
|
||||
});
|
||||
|
||||
// change collapsed in several items
|
||||
export const BatchOperation = atom(null, (get, set, payload) => {
|
||||
let prevOptions = get(collapseAtom);
|
||||
|
||||
let newOptions = {};
|
||||
|
||||
for (const item of payload.items) {
|
||||
newOptions[item.id] = payload.isCollapsed;
|
||||
}
|
||||
|
||||
if (payload.clear) {
|
||||
// clear object
|
||||
prevOptions = {};
|
||||
// clear localstorage
|
||||
localStorage.removeItem(PATH);
|
||||
}
|
||||
|
||||
const options = { ...prevOptions, ...newOptions };
|
||||
|
||||
set(collapseAtom, options);
|
||||
|
||||
localStorage.setItem(PATH, JSON.stringify(options));
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
const { atom } = require('jotai');
|
||||
|
||||
const PATH = 'option-gen';
|
||||
const initialValue = {
|
||||
cursor: 'locked',
|
||||
};
|
||||
|
||||
export const settingsAtom = atom(
|
||||
(get) => {
|
||||
const storedOptions = localStorage.getItem(PATH);
|
||||
if (storedOptions == null) return initialValue;
|
||||
|
||||
return JSON.parse(storedOptions);
|
||||
},
|
||||
(get, set, newValues) => {
|
||||
set(settingsAtom, newValues);
|
||||
localStorage.setItem(PATH, JSON.stringify(newValues));
|
||||
}
|
||||
);
|
||||
|
||||
// get a single option, if it exists
|
||||
export const SelectSetting = (setting) => {
|
||||
return atom((get) => get(settingsAtom)[setting]);
|
||||
};
|
||||
|
||||
// change a single item in object
|
||||
export const HandleOptions = atom(null, (get, set, payload) => {
|
||||
const updatedVal = {
|
||||
...get(settingsAtom),
|
||||
...payload,
|
||||
};
|
||||
set(settingsAtom, updatedVal);
|
||||
localStorage.setItem(PATH, JSON.stringify(updatedVal));
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
import io from 'socket.io-client';
|
||||
import { serverURL } from 'app/api/apiConstants';
|
||||
|
||||
@@ -8,7 +8,7 @@ export const useSocket = () => {
|
||||
return useContext(SocketContext);
|
||||
};
|
||||
|
||||
function SocketProvider(props) {
|
||||
function SocketProvider({ children }) {
|
||||
const [socket, setSocket] = useState();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -17,10 +17,7 @@ function SocketProvider(props) {
|
||||
return () => s.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SocketContext.Provider value={socket}>
|
||||
{props.children}
|
||||
</SocketContext.Provider>
|
||||
);
|
||||
return <SocketContext.Provider value={socket}>{children}</SocketContext.Provider>;
|
||||
}
|
||||
|
||||
export default SocketProvider;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* @description Validates two time entries
|
||||
* @param {number} timeStart
|
||||
* @param {number} timeEnd
|
||||
* @returns {{catch: string, value: boolean}}
|
||||
*/
|
||||
export const validateTimes = (timeStart, timeEnd) => {
|
||||
const validate = { value: true, catch: '' };
|
||||
if (timeStart > timeEnd) {
|
||||
validate.catch = 'Start time later than end time';
|
||||
}
|
||||
return validate;
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
// Roughly from useHooks - useLocalStorage
|
||||
|
||||
export const useLocalStorage = (key, initialValue) => {
|
||||
const [storedValue, setStoredValue] = useState(() => {
|
||||
try {
|
||||
const item = window.localStorage.getItem(`ontime-${key}`);
|
||||
return item ? JSON.parse(item) : initialValue;
|
||||
} catch (error) {
|
||||
return initialValue;
|
||||
}
|
||||
});
|
||||
|
||||
const setValue = (value) => {
|
||||
try {
|
||||
// Allow value to be a function so we have same API as useState
|
||||
const valueToStore =
|
||||
value instanceof Function ? value(storedValue) : value;
|
||||
|
||||
setStoredValue(valueToStore);
|
||||
window.localStorage.setItem(`ontime-${key}`, JSON.stringify(valueToStore));
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
return [storedValue, setValue];
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useMutation, useQueryClient } from 'react-query';
|
||||
import { EVENTS_TABLE } from '../api/apiConstants';
|
||||
|
||||
export default function useMutateEvents(mutation){
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation(mutation, {
|
||||
onMutate: async (newEvent) => {
|
||||
// cancel ongoing queries
|
||||
queryClient.cancelQueries(EVENTS_TABLE, { exact: true });
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousEvent = queryClient.getQueryData([EVENTS_TABLE, newEvent.id]);
|
||||
|
||||
// optimistically update object
|
||||
queryClient.setQueryData([EVENTS_TABLE, newEvent.id], newEvent);
|
||||
|
||||
// Return a context with the previous and new event
|
||||
return { previousEvent, newEvent };
|
||||
},
|
||||
|
||||
// Mutation fails, rollback undoes optimist update
|
||||
onError: (error, newEvent, context) => {
|
||||
queryClient.setQueryData([EVENTS_TABLE, context.newEvent.id], context.previousEvent);
|
||||
},
|
||||
|
||||
// Mutation finished, failed or successful
|
||||
// Fetch anyway, just to be sure
|
||||
onSettled: (newEvent) => {
|
||||
queryClient.invalidateQueries([EVENTS_TABLE, newEvent.id]);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
//
|
||||
// useTimeout React Hook
|
||||
//
|
||||
// React hook for delaying calls with time
|
||||
export const useTimeout = (
|
||||
callback, // function to call. No args passed.
|
||||
timeout = 0, // delay, ms (default: immediately put into JS Event Queue)
|
||||
{
|
||||
// manage re-render behavior.
|
||||
// by default, a re-render in your component will re-define the callback,
|
||||
// which will cause this timeout to cancel itself.
|
||||
// to avoid cancelling on re-renders (but still cancel on unmounts),
|
||||
// set `persistRenders: true,`.
|
||||
persistRenders = false,
|
||||
} = {},
|
||||
// These dependencies are injected for testing purposes.
|
||||
// (pure functions - where all dependencies are arguments - is often easier to test)
|
||||
_setTimeout = setTimeout,
|
||||
_clearTimeout = clearTimeout,
|
||||
_useEffect = useEffect
|
||||
) => {
|
||||
let timeoutId;
|
||||
const cancel = () => timeoutId && _clearTimeout(timeoutId);
|
||||
|
||||
_useEffect(
|
||||
() => {
|
||||
timeoutId = _setTimeout(callback, timeout);
|
||||
return cancel;
|
||||
},
|
||||
persistRenders
|
||||
? [_setTimeout, _clearTimeout]
|
||||
: [callback, timeout, _setTimeout, _clearTimeout]
|
||||
);
|
||||
|
||||
return cancel;
|
||||
};
|
||||
@@ -2,7 +2,7 @@ const dummy = new Date();
|
||||
|
||||
export const sampleData = {
|
||||
presenterMessage: {
|
||||
text: 'Only the presenter sees this',
|
||||
text: 'Only the timer sees this',
|
||||
active: false,
|
||||
},
|
||||
publicMessage: {
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export const userConfig = {
|
||||
timerColorOnPause: '#555',
|
||||
timerColorOnRunning: '#FFF',
|
||||
timerColorOnMessage: '#CCC',
|
||||
timerColorOnTimeOver: '#F00',
|
||||
overTimeText: '',
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export const clamp = (num, a, b) =>
|
||||
Math.max(Math.min(num, Math.max(a, b)), Math.min(a, b));
|
||||
@@ -0,0 +1,27 @@
|
||||
import { validateAlias } from '../aliases';
|
||||
|
||||
describe('An alias fails if incorrect', () => {
|
||||
|
||||
const testsToFail = [
|
||||
// no empty
|
||||
'',
|
||||
// no https, http or www
|
||||
'https://www.test.com',
|
||||
'http://www.test.com',
|
||||
'www.test.com',
|
||||
// no hostname
|
||||
'localhost/test',
|
||||
'127.0.0.1/test',
|
||||
'0.0.0.0/test',
|
||||
// no editor
|
||||
'editor',
|
||||
'editor?test'
|
||||
];
|
||||
|
||||
testsToFail.forEach((t) => (
|
||||
test(`${t}`, () => {
|
||||
expect(validateAlias(t).status).toBeFalsy();
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { clamp } from '../math';
|
||||
|
||||
test('Clamps a set of numbers correctly', () => {
|
||||
const testCases = [
|
||||
{ num: 10, min: 0, max: 20, result: 10 },
|
||||
{ num: 0, min: 0, max: 20, result: 0 },
|
||||
{ num: 20, min: 0, max: 20, result: 20 },
|
||||
{ num: 20, min: 0, max: 20, result: 20 },
|
||||
{ num: -20, min: 0, max: 20, result: 0 },
|
||||
{ num: -0, min: 0, max: 20, result: 0 },
|
||||
{ num: -50, min: -30, max: -20, result: -30 },
|
||||
{ num: -50, min: 0, max: 0, result: 0 },
|
||||
{ num: 50.5, min: 0, max: 100, result: 50.5 },
|
||||
{ num: 50, min: 0, max: 20.32, result: 20.32 },
|
||||
{ num: 10, min: 20.32, max: 40, result: 20.32 }
|
||||
];
|
||||
|
||||
testCases.forEach((t) => (
|
||||
expect(clamp(t.num, t.min, t.max)).toBe(t.result)
|
||||
));
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Validates an alias against defined parameters
|
||||
* @param {string} alias
|
||||
* @returns {{message: string, status: boolean}}
|
||||
*/
|
||||
export const validateAlias = (alias) => {
|
||||
|
||||
const valid = { status: true, message: 'ok' };
|
||||
|
||||
if (alias === '' || alias == null) {
|
||||
// cannot be empty
|
||||
valid.status = false;
|
||||
valid.message = 'should not be empty';
|
||||
} else if (alias.includes('http') || alias.includes('https') || alias.includes('www')) {
|
||||
// cannot contain http, https or www
|
||||
valid.status = false;
|
||||
valid.message = 'should not include http, https, www';
|
||||
} else if (alias.includes('127.0.0.1') || alias.includes('localhost') || alias.includes('0.0.0.0')) {
|
||||
// aliases cannot contain hostname
|
||||
valid.status = false;
|
||||
valid.message = 'should not include hostname';
|
||||
} else if (alias.includes('editor')) {
|
||||
// no editor
|
||||
valid.status = false;
|
||||
valid.message = 'No aliases to editor page allowed';
|
||||
}
|
||||
|
||||
return valid;
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Clamps a value between a min and a max
|
||||
* @param {number} num - Value to clamp
|
||||
* @param {number} min - min value
|
||||
* @param {number} max - max value
|
||||
* @returns {number}
|
||||
*/
|
||||
export const clamp = (num, min, max) =>
|
||||
Math.max(Math.min(num, Math.max(min, max)), Math.min(min, max));
|
||||
Binary file not shown.
@@ -1,5 +1,5 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiPlus } from 'react-icons/fi';
|
||||
import { FiPlus } from '@react-icons/all-files/fi/FiPlus';
|
||||
|
||||
export default function AddIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { FiChevronsDown } from 'react-icons/fi';
|
||||
import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
|
||||
|
||||
export default function ApplyIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
const { clickhandler, size, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label='Apply delays'>
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
icon={<FiChevronsDown />}
|
||||
size={size || 'xs'}
|
||||
icon={<FiCheck />}
|
||||
colorScheme='orange'
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiMinusCircle } from 'react-icons/fi';
|
||||
import { FiMinusCircle } from '@react-icons/all-files/fi/FiMinusCircle';
|
||||
|
||||
export default function BlockIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { FiChevronsUp } from 'react-icons/fi';
|
||||
import { FiChevronsUp } from '@react-icons/all-files/fi/FiChevronsUp';
|
||||
|
||||
export default function CollapseBtn(props) {
|
||||
const { clickhandler } = props;
|
||||
const { clickhandler, size } = props;
|
||||
return (
|
||||
<Tooltip label='Collapse all'>
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
size={size || 'xs'}
|
||||
icon={<FiChevronsUp />}
|
||||
colorScheme='white'
|
||||
variant='outline'
|
||||
background='#fff1'
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from '@chakra-ui/button';
|
||||
import { FiTarget } from 'react-icons/fi';
|
||||
import { FiTarget } from '@react-icons/all-files/fi/FiTarget';
|
||||
|
||||
export default function CurrentBtn(props) {
|
||||
const { clickhandler, active } = props;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { IoCaretDown } from '@react-icons/all-files/io5/IoCaretDown';
|
||||
|
||||
export default function CursorDownBtn(props) {
|
||||
const { clickhandler, active, ref, size } = props;
|
||||
return (
|
||||
<Tooltip label='Move cursor down Alt + ↓'>
|
||||
<IconButton
|
||||
ref={ref}
|
||||
size={size || 'xs'}
|
||||
icon={<IoCaretDown />}
|
||||
color={active ? 'pink.100' : 'pink.300'}
|
||||
borderColor={active ? undefined : 'pink.300'}
|
||||
backgroundColor={active ? 'pink.400' : undefined}
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
+7
-5
@@ -1,14 +1,15 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { FiTarget } from 'react-icons/fi';
|
||||
import { FiTarget } from '@react-icons/all-files/fi/FiTarget';
|
||||
|
||||
export default function LockIconBtn(props) {
|
||||
const { clickhandler, active, ref } = props;
|
||||
export default function CursorLockedBtn(props) {
|
||||
const { clickhandler, active, ref, size, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label='Lock cursor to current'>
|
||||
<IconButton
|
||||
<IconButton
|
||||
ref={ref}
|
||||
size={props.size || 'xs'}
|
||||
size={size || 'xs'}
|
||||
icon={<FiTarget />}
|
||||
color={active ? 'pink.100' : 'pink.300'}
|
||||
borderColor={active ? undefined : 'pink.300'}
|
||||
@@ -16,6 +17,7 @@ export default function LockIconBtn(props) {
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { IoCaretUp } from '@react-icons/all-files/io5/IoCaretUp';
|
||||
|
||||
export default function CursorUpBtn(props) {
|
||||
const { clickhandler, active, ref, size } = props;
|
||||
return (
|
||||
<Tooltip label='Move cursor up Alt + ↑'>
|
||||
<IconButton
|
||||
ref={ref}
|
||||
size={size || 'xs'}
|
||||
icon={<IoCaretUp />}
|
||||
color={active ? 'pink.100' : 'pink.300'}
|
||||
borderColor={active ? undefined : 'pink.300'}
|
||||
backgroundColor={active ? 'pink.400' : undefined}
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiClock } from 'react-icons/fi';
|
||||
import { FiClock } from '@react-icons/all-files/fi/FiClock';
|
||||
|
||||
export default function DelayIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { useState } from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { useState } from 'react';
|
||||
import { FiMinus } from 'react-icons/fi';
|
||||
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function DeleteIconBtn(props) {
|
||||
const { actionHandler, ...rest } = props;
|
||||
const { actionHandler, size, ...rest } = props;
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleClick = () => {
|
||||
@@ -12,15 +13,17 @@ export default function DeleteIconBtn(props) {
|
||||
};
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
icon={<FiMinus />}
|
||||
colorScheme='red'
|
||||
onClick={handleClick}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
disabled={loading}
|
||||
isLoading={loading}
|
||||
{...rest}
|
||||
/>
|
||||
<Tooltip label='Delete'>
|
||||
<IconButton
|
||||
size={size || 'xs'}
|
||||
icon={<IoRemove />}
|
||||
colorScheme='red'
|
||||
onClick={handleClick}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
disabled={loading}
|
||||
isLoading={loading}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { IoCloseSharp } from '@react-icons/all-files/io5/IoCloseSharp';
|
||||
import { IoCheckmarkSharp } from '@react-icons/all-files/io5/IoCheckmarkSharp';
|
||||
import { Button } from '@chakra-ui/button';
|
||||
|
||||
export default function EnableBtn(props) {
|
||||
const { active, text, actionHandler, size } = props;
|
||||
return (
|
||||
<Button
|
||||
size={size || 'xs'}
|
||||
leftIcon={active ? <IoCheckmarkSharp /> : <IoCloseSharp />}
|
||||
colorScheme='blue'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={actionHandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
>
|
||||
{text}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { FiChevronsDown } from 'react-icons/fi';
|
||||
import { FiChevronsDown } from '@react-icons/all-files/fi/FiChevronsDown';
|
||||
|
||||
export default function ExpandBtn(props) {
|
||||
const { clickhandler } = props;
|
||||
const { clickhandler, size } = props;
|
||||
return (
|
||||
<Tooltip label='Expand all'>
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
size={size || 'xs'}
|
||||
icon={<FiChevronsDown />}
|
||||
colorScheme='white'
|
||||
variant='outline'
|
||||
background='#fff1'
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
/>
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiSkipForward } from 'react-icons/fi';
|
||||
import { IoPlaySkipForward } from '@react-icons/all-files/io5/IoPlaySkipForward';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function NextIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
const { clickhandler, disabled, ...rest } = props;
|
||||
return (
|
||||
<IconButton
|
||||
icon={<FiSkipForward />}
|
||||
colorScheme='whiteAlpha'
|
||||
backgroundColor='#ffffff11'
|
||||
variant='outline'
|
||||
onClick={clickhandler}
|
||||
width={90}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
<Tooltip label='Next event' openDelay={500} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoPlaySkipForward size='22px' />}
|
||||
colorScheme='whiteAlpha'
|
||||
backgroundColor='#ffffff11'
|
||||
variant='outline'
|
||||
onClick={clickhandler}
|
||||
width={90}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { IoMicSharp } from '@react-icons/all-files/io5/IoMicSharp';
|
||||
import { IoMicOffOutline } from '@react-icons/all-files/io5/IoMicOffOutline';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function OnAirIconBtn(props) {
|
||||
const { actionHandler, active, size, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label={active ? 'Go Off Air' : 'Go On Air'} openDelay={500}>
|
||||
<IconButton
|
||||
size={size || 'xs'}
|
||||
icon={active ? <IoMicSharp size='24px' /> : <IoMicOffOutline size='24px' />}
|
||||
colorScheme='blue'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={() => actionHandler('update', { field: 'isPublic', value: !active })}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +1,21 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiPause } from 'react-icons/fi';
|
||||
import { IoPause } from '@react-icons/all-files/io5/IoPause';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function PauseIconBtn(props) {
|
||||
const { clickhandler, active, ...rest } = props;
|
||||
const { clickhandler, active, disabled, ...rest } = props;
|
||||
return (
|
||||
<IconButton
|
||||
icon={<FiPause />}
|
||||
colorScheme='orange'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
width={120}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
<Tooltip label='Pause timer' openDelay={500} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoPause size='24px' />}
|
||||
colorScheme='orange'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
width={120}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiSkipBack } from 'react-icons/fi';
|
||||
import { IoPlaySkipBack } from '@react-icons/all-files/io5/IoPlaySkipBack';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function PrevIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
const { clickhandler, disabled, ...rest } = props;
|
||||
return (
|
||||
<IconButton
|
||||
icon={<FiSkipBack />}
|
||||
colorScheme='whiteAlpha'
|
||||
backgroundColor='#ffffff11'
|
||||
variant='outline'
|
||||
onClick={clickhandler}
|
||||
width={90}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
<Tooltip label='Previous event' openDelay={500} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoPlaySkipBack size='22px' />}
|
||||
colorScheme='whiteAlpha'
|
||||
backgroundColor='#ffffff11'
|
||||
variant='outline'
|
||||
onClick={clickhandler}
|
||||
width={90}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { FiUsers } from 'react-icons/fi';
|
||||
import { FiUsers } from '@react-icons/all-files/fi/FiUsers';
|
||||
|
||||
export default function PublicIconBtn(props) {
|
||||
const { actionHandler, active, ...rest } = props;
|
||||
const { actionHandler, active, size, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label={active ? 'Make event private' : 'Make event public'}>
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
size={size || 'xs'}
|
||||
icon={<FiUsers />}
|
||||
colorScheme='blue'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={() =>
|
||||
actionHandler('update', { field: 'isPublic', value: !active })
|
||||
}
|
||||
onClick={() => actionHandler('update', { field: 'isPublic', value: !active })}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiRefreshCcw } from 'react-icons/fi';
|
||||
import { IoArrowUndo } from '@react-icons/all-files/io5/IoArrowUndo';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function ReloadIconButton(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
const { clickhandler, disabled, ...rest } = props;
|
||||
return (
|
||||
<IconButton
|
||||
icon={<FiRefreshCcw />}
|
||||
colorScheme='whiteAlpha'
|
||||
backgroundColor='#ffffff05'
|
||||
variant='outline'
|
||||
onClick={clickhandler}
|
||||
width={90}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
<Tooltip label='Reload event' openDelay={500} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoArrowUndo size='22px' />}
|
||||
colorScheme='whiteAlpha'
|
||||
backgroundColor='#ffffff05'
|
||||
variant='outline'
|
||||
onClick={clickhandler}
|
||||
width={90}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiClock } from 'react-icons/fi';
|
||||
import { IoTimeOutline } from '@react-icons/all-files/io5/IoTimeOutline';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function RollIconBtn(props) {
|
||||
const { clickhandler, active, ...rest } = props;
|
||||
const { clickhandler, active, disabled, ...rest } = props;
|
||||
return (
|
||||
<IconButton
|
||||
icon={<FiClock />}
|
||||
colorScheme='blue'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
width={120}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
<Tooltip label='Roll mode' openDelay={500} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoTimeOutline size='24px' />}
|
||||
colorScheme='blue'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
width={120}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiPlay } from 'react-icons/fi';
|
||||
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function StartIconBtn(props) {
|
||||
const { clickhandler, active, ...rest } = props;
|
||||
const { clickhandler, active, disabled, ...rest } = props;
|
||||
return (
|
||||
<IconButton
|
||||
icon={<FiPlay />}
|
||||
colorScheme='green'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
width={120}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
<Tooltip label='Start timer' openDelay={500} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoPlay size='24px' />}
|
||||
colorScheme='green'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
width={120}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiTrash2 } from 'react-icons/fi';
|
||||
import { FiTrash2 } from '@react-icons/all-files/fi/FiTrash2';
|
||||
|
||||
export default function TrashIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiXOctagon } from 'react-icons/fi';
|
||||
import { IoStop } from '@react-icons/all-files/io5/IoStop';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function UnloadIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
const { clickhandler, disabled, ...rest } = props;
|
||||
return (
|
||||
<IconButton
|
||||
icon={<FiXOctagon />}
|
||||
colorScheme='whiteAlpha'
|
||||
backgroundColor='#ffffff05'
|
||||
variant='outline'
|
||||
onClick={clickhandler}
|
||||
width={90}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
<Tooltip label='Unload event' openDelay={500} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoStop size='22px' />}
|
||||
colorScheme='red'
|
||||
variant='outline'
|
||||
onClick={clickhandler}
|
||||
width={90}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiSun } from 'react-icons/fi';
|
||||
import { IoSunny } from '@react-icons/all-files/io5/IoSunny';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function VisibleIconBtn(props) {
|
||||
const { actionHandler, active, ...rest } = props;
|
||||
const { actionHandler, active, size, ...rest } = props;
|
||||
return (
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
icon={<FiSun />}
|
||||
colorScheme='blue'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={() =>
|
||||
actionHandler('update', { field: 'isPublic', value: !active })
|
||||
}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
<Tooltip label={active ? 'Make invisible' : 'Make visible'} openDelay={500}>
|
||||
<IconButton
|
||||
size={size || 'xs'}
|
||||
icon={<IoSunny size='18px' />}
|
||||
colorScheme='blue'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={() =>
|
||||
actionHandler('update', { field: 'isPublic', value: !active })
|
||||
}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Icon } from '@chakra-ui/react';
|
||||
import { FiChevronUp } from '@react-icons/all-files/fi/FiChevronUp';
|
||||
import style from './CollapseBar.module.scss';
|
||||
|
||||
export default function CollapseBar(props) {
|
||||
const { title = 'Collapse bar', isCollapsed, onClick, roll } = props;
|
||||
|
||||
return (
|
||||
<div className={roll ? style.headerRoll : style.header}>
|
||||
{title}
|
||||
<Icon
|
||||
className={isCollapsed ? style.moreCollapsed : style.moreExpanded}
|
||||
as={FiChevronUp}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
CollapseBar.propTypes = {
|
||||
title: PropTypes.string,
|
||||
isCollapsed: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
roll: PropTypes.bool,
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.header,
|
||||
.headerRoll {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: 0.9em;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header {
|
||||
color: $header-gray;
|
||||
}
|
||||
|
||||
.headerRoll {
|
||||
color: $ontime-roll;
|
||||
}
|
||||
|
||||
.moreExpanded,
|
||||
.moreCollapsed {
|
||||
cursor: pointer;
|
||||
color: $text-white;
|
||||
}
|
||||
|
||||
.moreExpanded {
|
||||
transform: scaleY(-1);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.moreCollapsed {
|
||||
transform: scaleY(1);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
@@ -1,23 +1,28 @@
|
||||
import { memo } from 'react';
|
||||
import { formatDisplay } from 'common/dateConfig';
|
||||
import styles from './Countdown.module.css';
|
||||
|
||||
const Countdown = ({ time, small, negative, hideZeroHours }) => {
|
||||
let display = '-- : -- : --';
|
||||
import React, { memo } from 'react';
|
||||
import { formatDisplay } from 'common/utils/dateConfig';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './Countdown.module.scss';
|
||||
|
||||
const Countdown = ({ time, small, isNegative, hideZeroHours }) => {
|
||||
// prepare display string
|
||||
if (time != null && !isNaN(time))
|
||||
display = formatDisplay(Math.abs(time), hideZeroHours);
|
||||
const colour = negative ? '#ff7597' : '#fffffa';
|
||||
const display =
|
||||
time != null && !isNaN(time) ? formatDisplay(time, hideZeroHours) : '-- : -- : --';
|
||||
|
||||
const classes = `${small ? styles.countdownClockSmall : styles.countdownClock}
|
||||
${isNegative ? styles.negative : ''}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={small ? styles.countdownClockSmall : styles.countdownClock}
|
||||
style={{ color: colour }}
|
||||
>
|
||||
<div className={classes}>
|
||||
{display}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Countdown);
|
||||
|
||||
Countdown.propTypes = {
|
||||
time: PropTypes.number,
|
||||
small: PropTypes.bool,
|
||||
isNegative: PropTypes.bool,
|
||||
hideZeroHours: PropTypes.bool,
|
||||
};
|
||||
|
||||
+6
-2
@@ -1,10 +1,11 @@
|
||||
/* @import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;600;800&display=swap'); */
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
/* Common */
|
||||
.countdownClock,
|
||||
.countdownClockSmall {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
line-height: 0.9;
|
||||
color: $text-white;
|
||||
}
|
||||
|
||||
/* Viewers */
|
||||
@@ -21,5 +22,8 @@
|
||||
text-align: center;
|
||||
letter-spacing: 0.1em;
|
||||
font-weight: 600;
|
||||
color: #fffffa;
|
||||
}
|
||||
|
||||
.negative {
|
||||
color: $ontime-pink;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/* eslint-disable react/destructuring-assignment */
|
||||
import React from 'react';
|
||||
import { LoggingContext } from '../../../app/context/LoggingContext';
|
||||
import style from './ErrorBoundary.module.scss';
|
||||
|
||||
class ErrorBoundary extends React.Component {
|
||||
static contextType = LoggingContext;
|
||||
reportContent = '';
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { error: null, errorInfo: null };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error) {
|
||||
// Update state so next render shows fallback UI.
|
||||
return { errorMessage: error.toString() };
|
||||
}
|
||||
|
||||
componentDidCatch(error, info) {
|
||||
this.setState({
|
||||
error: error,
|
||||
errorInfo: info,
|
||||
});
|
||||
this.context.emitError(error.toString());
|
||||
this.reportContent = `${error} ${info.componentStack}`;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.errorMessage) {
|
||||
return (
|
||||
<div className={style.errorContainer}>
|
||||
<div>
|
||||
<p className={style.error}>:/</p>
|
||||
<p>Something went wrong</p>
|
||||
<p
|
||||
className={style.report}
|
||||
onClick={() => navigator.clipboard.writeText(this.reportContent)}
|
||||
>
|
||||
Copy error
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
||||
@@ -0,0 +1,28 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.errorContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
background-color: #121212;
|
||||
color: white;
|
||||
|
||||
.error {
|
||||
color: $ontime-pink;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.report {
|
||||
text-decoration: underline $ontime-pink;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.report:hover {
|
||||
color: $ontime-pink;
|
||||
}
|
||||
|
||||
.report:active {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,42 @@
|
||||
import React, { useContext } from 'react';
|
||||
import EditableTimer from 'common/input/EditableTimer';
|
||||
import { showWarningToast } from 'common/helpers/toastManager';
|
||||
import { LoggingContext } from '../../../app/context/LoggingContext';
|
||||
import { validateTimes } from '../../../app/entryValidator';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function EventTimes(props) {
|
||||
const { actionHandler, delay, timeStart, timeEnd } = props;
|
||||
const { actionHandler, delay, timeStart, timeEnd, previousEnd } = props;
|
||||
const { emitWarning } = useContext(LoggingContext);
|
||||
|
||||
const handleValidate = (entry, v) => {
|
||||
// we dont inforce validation here
|
||||
|
||||
if (v == null || timeStart == null || timeEnd == null) return true;
|
||||
/**
|
||||
* This code is duplicated from EventTimesVertical
|
||||
* @description Validates a time input against its pair
|
||||
* @param {string} entry - field to validate: timeStart, timeEnd, durationOverride
|
||||
* @param {number} val - field value
|
||||
* @return {boolean}
|
||||
*/
|
||||
const handleValidate = (entry, val) => {
|
||||
if (val == null || timeStart == null || timeEnd == null) return true;
|
||||
if (timeStart === 0) return true;
|
||||
|
||||
let validate = { value: true, catch: '' };
|
||||
if (entry === 'timeStart' && v > timeEnd)
|
||||
validate.catch = 'Start time later than end time';
|
||||
else if (entry === 'timeEnd' && v < timeStart)
|
||||
validate.catch = 'End time earlier than start time';
|
||||
let start = timeStart;
|
||||
let end = timeEnd;
|
||||
if (entry === 'timeStart') {
|
||||
start = val;
|
||||
} else if (entry === 'timeEnd') {
|
||||
end = val;
|
||||
} else if (entry === 'durationOverride'){
|
||||
return true;
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
||||
if (validate.catch !== '')
|
||||
showWarningToast('Time Input Warning', validate.catch);
|
||||
return validate.value;
|
||||
const valid = validateTimes(start, end);
|
||||
// give warning but not enforce validation
|
||||
if (!valid.value) {
|
||||
emitWarning(`Time Input Warning: ${valid.catch}`);
|
||||
}
|
||||
return valid.value;
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -29,6 +47,7 @@ export default function EventTimes(props) {
|
||||
actionHandler={actionHandler}
|
||||
time={timeStart}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<EditableTimer
|
||||
name='timeEnd'
|
||||
@@ -36,7 +55,16 @@ export default function EventTimes(props) {
|
||||
actionHandler={actionHandler}
|
||||
time={timeEnd}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
EventTimes.propTypes = {
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
delay: PropTypes.number,
|
||||
timeStart: PropTypes.number,
|
||||
timeEnd: PropTypes.number,
|
||||
previousEnd: PropTypes.number,
|
||||
};
|
||||
|
||||
@@ -1,121 +1,68 @@
|
||||
import EditableTimer from 'common/input/EditableTimer';
|
||||
import { showWarningToast } from 'common/helpers/toastManager';
|
||||
import { stringFromMillis } from 'common/dateConfig';
|
||||
|
||||
const label = {
|
||||
fontSize: '0.75em',
|
||||
color: '#aaa',
|
||||
};
|
||||
|
||||
const TimesDelayed = (props) => {
|
||||
const { handleValidate, actionHandler, delay, timeStart, timeEnd, duration } =
|
||||
props;
|
||||
|
||||
const scheduledStart = stringFromMillis(timeStart, false);
|
||||
const scheduledEnd = stringFromMillis(timeEnd, false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<span style={label}>
|
||||
Start <span>{scheduledStart}</span>
|
||||
</span>
|
||||
<EditableTimer
|
||||
name='timeStart'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeStart}
|
||||
delay={delay}
|
||||
/>
|
||||
<span style={label}>
|
||||
End <span>{scheduledEnd}</span>
|
||||
</span>
|
||||
<EditableTimer
|
||||
name='timeEnd'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeEnd}
|
||||
delay={delay}
|
||||
/>
|
||||
<span style={label}>Duration</span>
|
||||
<EditableTimer
|
||||
name='durationOverride'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={duration}
|
||||
delay={0}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Times = (props) => {
|
||||
const { handleValidate, actionHandler, timeStart, timeEnd, duration } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<span style={label}>Start</span>
|
||||
<EditableTimer
|
||||
name='timeStart'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeStart}
|
||||
delay={0}
|
||||
/>
|
||||
<span style={label}>End</span>
|
||||
<EditableTimer
|
||||
name='timeEnd'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeEnd}
|
||||
delay={0}
|
||||
/>
|
||||
<span style={label}>Duration</span>
|
||||
<EditableTimer
|
||||
name='durationOverride'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={duration}
|
||||
delay={0}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
import React, { useContext } from 'react';
|
||||
import { LoggingContext } from '../../../app/context/LoggingContext';
|
||||
import { validateTimes } from '../../../app/entryValidator';
|
||||
import PropTypes from 'prop-types';
|
||||
import TimesDelayed from './TimesDelayed';
|
||||
import Times from './Times';
|
||||
|
||||
export default function EventTimesVertical(props) {
|
||||
const { delay, timeStart, timeEnd, duration } = props;
|
||||
const handleValidate = (entry, v) => {
|
||||
// we dont enforce validation here
|
||||
const { delay, timeStart, timeEnd, duration, previousEnd, actionHandler } = props;
|
||||
const { emitWarning } = useContext(LoggingContext);
|
||||
|
||||
if (v == null || timeStart == null || timeEnd == null) return true;
|
||||
/**
|
||||
* This code is duplicated from EventTimes
|
||||
* @description Validates a time input against its pair
|
||||
* @param {string} entry - field to validate: timeStart, timeEnd, durationOverride
|
||||
* @param {number} val - field value
|
||||
* @return {boolean}
|
||||
*/
|
||||
const handleValidate = (entry, val) => {
|
||||
if (val == null || timeStart == null || timeEnd == null) return true;
|
||||
if (timeStart === 0) return true;
|
||||
|
||||
let validate = { value: true, catch: '' };
|
||||
if (entry === 'timeStart' && v > timeEnd)
|
||||
validate.catch = 'Start time later than end time';
|
||||
else if (entry === 'timeEnd' && v < timeStart)
|
||||
validate.catch = 'End time earlier than start time';
|
||||
let start = timeStart;
|
||||
let end = timeEnd;
|
||||
if (entry === 'timeStart') {
|
||||
start = val;
|
||||
} else if (entry === 'timeEnd') {
|
||||
end = val;
|
||||
} else return entry === 'durationOverride';
|
||||
|
||||
if (validate.catch !== '')
|
||||
showWarningToast('Time Input Warning', validate.catch);
|
||||
return validate.value;
|
||||
const valid = validateTimes(start, end);
|
||||
// give warning but not enforce validation
|
||||
if (!valid.value) {
|
||||
emitWarning(`Time Input Warning: ${valid.catch}`);
|
||||
}
|
||||
return valid.value;
|
||||
};
|
||||
|
||||
return (delay != null) & (delay > 0) ? (
|
||||
return delay != null && delay !== 0 ? (
|
||||
<TimesDelayed
|
||||
handleValidate={handleValidate}
|
||||
actionHandler={props.actionHandler}
|
||||
actionHandler={actionHandler}
|
||||
delay={delay}
|
||||
timeStart={timeStart}
|
||||
timeEnd={timeEnd}
|
||||
duration={duration}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
) : (
|
||||
<Times
|
||||
handleValidate={handleValidate}
|
||||
actionHandler={props.actionHandler}
|
||||
actionHandler={actionHandler}
|
||||
timeStart={timeStart}
|
||||
timeEnd={timeEnd}
|
||||
duration={duration}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
EventTimesVertical.propTypes = {
|
||||
delay: PropTypes.number,
|
||||
timeStart: PropTypes.number,
|
||||
timeEnd: PropTypes.number,
|
||||
duration: PropTypes.number,
|
||||
previousEnd: PropTypes.number,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import EditableTimer from '../../input/EditableTimer';
|
||||
import PropTypes from 'prop-types';
|
||||
import style from './Times.module.scss'
|
||||
|
||||
export default function Times(props) {
|
||||
const { handleValidate, actionHandler, timeStart, timeEnd, duration, previousEnd } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={style.label}>Start</span>
|
||||
<EditableTimer
|
||||
name='timeStart'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeStart}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span className={style.label}>End</span>
|
||||
<EditableTimer
|
||||
name='timeEnd'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeEnd}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span className={style.label}>Duration</span>
|
||||
<EditableTimer
|
||||
name='durationOverride'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={duration}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Times.propTypes = {
|
||||
handleValidate: PropTypes.func.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
timeStart: PropTypes.number,
|
||||
timeEnd: PropTypes.number,
|
||||
duration: PropTypes.number,
|
||||
previousEnd: PropTypes.number,
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.label {
|
||||
font-size: 0.75em;
|
||||
color: $label-gray;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import EditableTimer from '../../input/EditableTimer';
|
||||
import PropTypes from 'prop-types';
|
||||
import { stringFromMillis } from '../../utils/time';
|
||||
import style from './Times.module.scss'
|
||||
|
||||
export default function TimesDelayed(props) {
|
||||
const { handleValidate, actionHandler, delay, timeStart, timeEnd, duration, previousEnd } = props;
|
||||
|
||||
const scheduledStart = stringFromMillis(timeStart, false);
|
||||
const scheduledEnd = stringFromMillis(timeEnd, false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={style.label}>
|
||||
Start <span>{scheduledStart}</span>
|
||||
</span>
|
||||
<EditableTimer
|
||||
name='timeStart'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeStart}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span className={style.label}>
|
||||
End <span>{scheduledEnd}</span>
|
||||
</span>
|
||||
<EditableTimer
|
||||
name='timeEnd'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeEnd}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span className={style.label}>Duration</span>
|
||||
<EditableTimer
|
||||
name='durationOverride'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={duration}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
TimesDelayed.propTypes = {
|
||||
handleValidate: PropTypes.func.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
delay: PropTypes.number,
|
||||
timeStart: PropTypes.number,
|
||||
timeEnd: PropTypes.number,
|
||||
duration: PropTypes.number,
|
||||
previousEnd: PropTypes.number,
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import { clamp } from 'app/utils';
|
||||
import styles from './MyProgressBar.module.css';
|
||||
import React from 'react';
|
||||
import { clamp } from 'app/utils/math';
|
||||
import styles from './MyProgressBar.module.scss';
|
||||
|
||||
export default function MyProgressBar(props) {
|
||||
const { now, complete, showElapsed } = props;
|
||||
|
||||
+5
-4
@@ -1,22 +1,23 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.progress,
|
||||
.progressCountdown {
|
||||
height: 2vh;
|
||||
background-color: rgba(255, 255, 255, 0.13);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.progress {
|
||||
background-color: rgba(255, 255, 255, 0.13);
|
||||
background-color: $bg-gray-900;
|
||||
}
|
||||
|
||||
.progressCountdown {
|
||||
background-color: #ff7597;
|
||||
background-color: $ontime-pink;
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
width: 100%;
|
||||
height: 2vh;
|
||||
background-color: rgb(255, 255, 255);
|
||||
background-color: $title-white;
|
||||
border-radius: 4px;
|
||||
transition: 1s linear;
|
||||
transition-property: width;
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Link, Redirect } from 'react-router-dom';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Image } from '@chakra-ui/react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useState, useEffect } from 'react';
|
||||
import tinykeys from 'tinykeys';
|
||||
import navlogo from 'assets/images/logos/LOGO-72.png';
|
||||
import style from './NavLogo.module.css';
|
||||
import style from './NavLogo.module.scss';
|
||||
|
||||
export default function NavLogo() {
|
||||
export default function NavLogo(props) {
|
||||
const { isHidden } = props;
|
||||
const [showNav, setShowNav] = useState(false);
|
||||
|
||||
const handleClick = () => {
|
||||
@@ -14,80 +15,71 @@ export default function NavLogo() {
|
||||
};
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
useEffect(() => {
|
||||
let unsubscribe = tinykeys(window, {
|
||||
Space: () => {
|
||||
setShowNav((s) => !s);
|
||||
},
|
||||
});
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
const handleKeyPress = useCallback((e) => {
|
||||
// handle held key
|
||||
if (e.repeat) return;
|
||||
// Space bar
|
||||
if (e.keyCode === 32) {
|
||||
setShowNav((s) => !s);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// attach the event listener
|
||||
document.addEventListener('keydown', handleKeyPress);
|
||||
|
||||
// remove the event listener
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyPress);
|
||||
};
|
||||
}, [handleKeyPress]);
|
||||
|
||||
const baseOpacity = isHidden ? 0 : 0.5;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0.5 }}
|
||||
initial={{ opacity: showNav ? 0.5 : baseOpacity }}
|
||||
whileHover={{ opacity: 1 }}
|
||||
className={style.navContainer}
|
||||
>
|
||||
<Image
|
||||
alt=''
|
||||
src={navlogo}
|
||||
className={style.logo}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
<Image alt='' src={navlogo} className={style.logo} onClick={handleClick} />
|
||||
<AnimatePresence>
|
||||
{showNav && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0, y: -50 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
exit={{ opacity: 0, scaleY: 0, y: -50 }}
|
||||
className={showNav ? style.nav : style.navHidden}
|
||||
className={style.nav}
|
||||
>
|
||||
<Link
|
||||
to='/speaker'
|
||||
className={style.navItem}
|
||||
tabIndex={1}
|
||||
onKeyDownCapture={() => <Redirect push to='/speaker' />}
|
||||
>
|
||||
Speaker
|
||||
<Link to='/timer' className={style.navItem} tabIndex={1}>
|
||||
Timer
|
||||
</Link>
|
||||
<Link
|
||||
to='/sm'
|
||||
className={style.navItem}
|
||||
tabIndex={2}
|
||||
onKeyDownCapture={() => <Redirect push to='/sm' />}
|
||||
>
|
||||
<Link to='/minimal' className={style.navItem} tabIndex={2}>
|
||||
Minimal Timer
|
||||
</Link>
|
||||
<Link to='/sm' className={style.navItem} tabIndex={3}>
|
||||
Backstage
|
||||
</Link>
|
||||
<Link
|
||||
to='/public'
|
||||
className={style.navItem}
|
||||
tabIndex={3}
|
||||
onKeyDownCapture={() => <Redirect push to='/public' />}
|
||||
>
|
||||
<Link to='/public' className={style.navItem} tabIndex={4}>
|
||||
Public
|
||||
</Link>
|
||||
<Link
|
||||
to='/lower'
|
||||
className={style.navItem}
|
||||
tabIndex={4}
|
||||
onKeyDownCapture={() => <Redirect push to='/lower' />}
|
||||
>
|
||||
<Link to='/lower' className={style.navItem} tabIndex={5}>
|
||||
Lower Thirds
|
||||
</Link>
|
||||
<Link
|
||||
to='/pip'
|
||||
className={style.navItem}
|
||||
tabIndex={4}
|
||||
onKeyDownCapture={() => <Redirect push to='/pip' />}
|
||||
>
|
||||
<Link to='/pip' className={style.navItem} tabIndex={6}>
|
||||
PIP
|
||||
</Link>
|
||||
<Link to='/studio' className={style.navItem} tabIndex={7}>
|
||||
Studio Clock
|
||||
</Link>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
NavLogo.propTypes = {
|
||||
isHidden: PropTypes.bool,
|
||||
};
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
.navContainer {
|
||||
position: absolute;
|
||||
right: 2vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
font-size: 1vw;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 3vw;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.nav {
|
||||
gap: 2vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.navItem {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
padding: 0.5vh 1vw;
|
||||
border-radius: 4px;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.navContainer {
|
||||
position: absolute;
|
||||
right: 2vw;
|
||||
top: 1vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
font-size: max(1vw, 16px);
|
||||
z-index: 10;
|
||||
|
||||
.logo {
|
||||
width: max(3vw, 32px);
|
||||
opacity: 0.5;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav,
|
||||
.showNav {
|
||||
gap: 2vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.navItem {
|
||||
background-color: $bg-overlay;
|
||||
padding: 0.5vh 1vw;
|
||||
border-radius: 4px;
|
||||
color: $text-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import style from './ProtectRoute.module.scss';
|
||||
import { HStack, PinInput, PinInputField } from '@chakra-ui/react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
|
||||
import { AppContext } from '../../../app/context/AppContext';
|
||||
|
||||
export default function ProtectRoute({ children }) {
|
||||
const isLocal =
|
||||
window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
|
||||
const [pin, setPin] = useState('');
|
||||
const [failed, setFailed] = useState(false);
|
||||
const { auth, validate } = useContext(AppContext);
|
||||
|
||||
const handleValidation = useCallback(() => {
|
||||
const r = validate(pin);
|
||||
if (!r) {
|
||||
setFailed(true);
|
||||
setPin('');
|
||||
}
|
||||
}, [pin, validate]);
|
||||
|
||||
// Set window title
|
||||
useEffect(() => {
|
||||
document.title = 'ontime';
|
||||
}, []);
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
const handleKeyPress = useCallback(
|
||||
(e) => {
|
||||
// handle held key
|
||||
if (e.repeat) return;
|
||||
// Space bar
|
||||
if (e.keyCode === 13) {
|
||||
handleValidation();
|
||||
}
|
||||
},
|
||||
[handleValidation],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// attach the event listener
|
||||
document.addEventListener('keydown', handleKeyPress);
|
||||
|
||||
// remove the event listener
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyPress);
|
||||
};
|
||||
}, [handleKeyPress]);
|
||||
|
||||
if (isLocal || auth) {
|
||||
return children;
|
||||
} else {
|
||||
return (
|
||||
<div className={style.container}>
|
||||
ontime
|
||||
<HStack spacing='10px' className={failed ? style.pin__failed : style.pin}>
|
||||
<PinInput
|
||||
type='alphanumeric'
|
||||
size='lg'
|
||||
mask
|
||||
autoFocus
|
||||
value={pin}
|
||||
onChange={(value) => {
|
||||
setFailed(false);
|
||||
setPin(value);
|
||||
}}
|
||||
>
|
||||
<PinInputField />
|
||||
<PinInputField />
|
||||
<PinInputField />
|
||||
<PinInputField />
|
||||
</PinInput>
|
||||
<IconButton
|
||||
aria-label='Enter'
|
||||
size='lg'
|
||||
isRound
|
||||
icon={<FiCheck />}
|
||||
style={{ fontSize: '1.5em' }}
|
||||
onClick={() => handleValidation()}
|
||||
/>
|
||||
</HStack>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ProtectRoute.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
@use '../../../styles/main' as *;
|
||||
@use '../../../styles/mixins' as *;
|
||||
|
||||
.container {
|
||||
background: $bg-black;
|
||||
|
||||
display: grid;
|
||||
place-content: center;
|
||||
height: 100vh;
|
||||
padding-bottom: 30vh;
|
||||
|
||||
color: $ontime-pink;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-weight: 200;
|
||||
text-align: center;
|
||||
font-size: 3vw;
|
||||
}
|
||||
|
||||
.pin,
|
||||
.pin__failed {
|
||||
padding: 20px;
|
||||
|
||||
input {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.pin__failed {
|
||||
input {
|
||||
animation: colourFade 1.5s ease;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes colourFade {
|
||||
from {
|
||||
background: $ontime-pink;
|
||||
}
|
||||
to {
|
||||
background: rgba($ontime-pink, 0);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import styles from './SmallTimer.module.css';
|
||||
|
||||
export default function SmallTimer({ label, time }) {
|
||||
return (
|
||||
<div className={styles.SmallTimer}>
|
||||
<div className={styles.label}>{label}</div>
|
||||
<div className={styles.timer}>{time}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
.smallTimer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.label,
|
||||
.timer {
|
||||
font-size: 1vw;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 1.5vw;
|
||||
color: #888;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.timer {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
|
||||
font-size: 4vw;
|
||||
letter-spacing: 0.125em;
|
||||
}
|
||||
@@ -1,32 +1,53 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import TodayItem from './TodayItem';
|
||||
import style from './Paginator.module.css';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useInterval } from 'app/hooks/useInterval';
|
||||
import PropTypes from 'prop-types';
|
||||
import style from './Paginator.module.scss';
|
||||
import Empty from '../../state/Empty';
|
||||
|
||||
export default function Paginator(props) {
|
||||
const { events, selectedId } = props;
|
||||
const LIMIT_PER_PAGE = props.limit || 8;
|
||||
const SCROLL_TIME = props.time * 1000 || 10000;
|
||||
const {
|
||||
events,
|
||||
selectedId,
|
||||
limit = 8,
|
||||
time = 10,
|
||||
isBackstage,
|
||||
setPageNumber,
|
||||
setCurrentPage,
|
||||
} = props;
|
||||
const LIMIT_PER_PAGE = limit;
|
||||
const SCROLL_TIME = time * 1000;
|
||||
const [numEvents, setNumEvents] = useState(0);
|
||||
const [page, setPage] = useState([]);
|
||||
const [pages, setPages] = useState(0);
|
||||
const [selPage, setSelPage] = useState(0);
|
||||
|
||||
// keep parent up to date
|
||||
useEffect(() => {
|
||||
if (setPageNumber) {
|
||||
setPageNumber(pages);
|
||||
}
|
||||
}, [setPageNumber, pages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (setCurrentPage) {
|
||||
setCurrentPage(selPage);
|
||||
}
|
||||
}, [setCurrentPage, selPage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (events == null) return;
|
||||
// how many events in list
|
||||
let n = events.length;
|
||||
const n = events.length;
|
||||
setNumEvents(n);
|
||||
|
||||
// how many paginated views
|
||||
let p = Math.ceil(n / LIMIT_PER_PAGE);
|
||||
setPages(p);
|
||||
setPages(Math.ceil(n / LIMIT_PER_PAGE));
|
||||
|
||||
// divide events in parts of LIMIT_PER_PAGE
|
||||
const eventStart = LIMIT_PER_PAGE * selPage;
|
||||
const eventEnd = LIMIT_PER_PAGE * (selPage + 1);
|
||||
let e = events.slice(eventStart, eventEnd);
|
||||
setPage(e);
|
||||
setPage(events.slice(eventStart, eventEnd));
|
||||
|
||||
// if array is completely in past, show depending on SCROLL_PAST
|
||||
}, [events, selPage, LIMIT_PER_PAGE]);
|
||||
@@ -41,17 +62,10 @@ export default function Paginator(props) {
|
||||
|
||||
let selectedState = 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={style.nav}>
|
||||
{pages > 1 &&
|
||||
[...Array(pages)].map((p, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={i === selPage ? style.navItemSelected : style.navItem}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
if (events?.length < 1) {
|
||||
return <Empty text='No events to show' />;
|
||||
} else {
|
||||
return (
|
||||
<div className={style.entries}>
|
||||
{page.map((e) => {
|
||||
if (e.id === selectedId) selectedState = 1;
|
||||
@@ -63,11 +77,22 @@ export default function Paginator(props) {
|
||||
timeStart={e.timeStart}
|
||||
timeEnd={e.timeEnd}
|
||||
title={e.title}
|
||||
colour={isBackstage ? e.colour : ''}
|
||||
backstageEvent={!e.isPublic}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Paginator.propTypes = {
|
||||
events: PropTypes.array,
|
||||
selectedId: PropTypes.string,
|
||||
limit: PropTypes.number,
|
||||
time: PropTypes.number,
|
||||
isBackstage: PropTypes.bool,
|
||||
setPageNumber: PropTypes.func,
|
||||
setCurrentPage: PropTypes.func,
|
||||
};
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
.entries {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.navItem,
|
||||
.navItemSelected {
|
||||
background-color: rgba(255, 255, 255, 0.39);
|
||||
width: 0.7vw;
|
||||
height: 0.7vw;
|
||||
border-radius: 0.35vw;
|
||||
}
|
||||
|
||||
.navItemSelected {
|
||||
background-color: rgba(255, 255, 255, 0.79);
|
||||
}
|
||||
|
||||
.nav {
|
||||
align-self: center;
|
||||
justify-content: flex-end;
|
||||
display: flex;
|
||||
margin-bottom: 2.5vh;
|
||||
}
|
||||
|
||||
.nav > div {
|
||||
margin-left: 0.5vw;
|
||||
}
|
||||
|
||||
.entryTimes {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 1.4vw;
|
||||
|
||||
min-width: 10vw;
|
||||
opacity: 0.9;
|
||||
letter-spacing: 0.035em;
|
||||
}
|
||||
|
||||
.entryTitle {
|
||||
align-self: center;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.entryPast,
|
||||
.entryNow,
|
||||
.entryFuture {
|
||||
display: flex;
|
||||
font-size: 1.3vw;
|
||||
padding: 0.2vh 1vw;
|
||||
margin-bottom: 1.5vh;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.entryPast {
|
||||
background-color: rgba(255, 255, 255, 0.01);
|
||||
font-size: 1.2vw;
|
||||
}
|
||||
|
||||
.entryPast > .entryTitle {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.entryFuture {
|
||||
background-color: rgba(255, 255, 255, 0.03);
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.entryNow {
|
||||
color: #fff;
|
||||
line-height: 5vh;
|
||||
background-color: rgba(255, 255, 255, 0.07);
|
||||
border-bottom: 0.5vh solid #ff7597aa;
|
||||
margin-left: -0.5em;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
|
||||
.backstageInd {
|
||||
color: #ff7597aa;
|
||||
margin-left: auto;
|
||||
margin-right: -0.5vw;
|
||||
}
|
||||
|
||||
.backstageInd::before {
|
||||
content: '*';
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.entries {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.entryTimes {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 1.4vw;
|
||||
|
||||
min-width: 10vw;
|
||||
opacity: 0.9;
|
||||
letter-spacing: 0.035em;
|
||||
}
|
||||
|
||||
.entryTitle {
|
||||
align-self: center;
|
||||
@include ellipsis;
|
||||
}
|
||||
|
||||
.entryPast,
|
||||
.entryNow,
|
||||
.entryFuture {
|
||||
display: flex;
|
||||
font-size: 1.3vw;
|
||||
padding: 0.2vh 1vw;
|
||||
margin-bottom: 1.5vh;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.entryPast {
|
||||
font-size: 1.2vw;
|
||||
}
|
||||
|
||||
.entryPast > .entryTitle {
|
||||
color: $text-gray-disabled;
|
||||
}
|
||||
|
||||
.entryNow,
|
||||
.entryFuture {
|
||||
color: $text-white;
|
||||
}
|
||||
|
||||
.entryFuture {
|
||||
background-color: $bg-gray-950;
|
||||
}
|
||||
|
||||
.entryNow {
|
||||
line-height: 5vh;
|
||||
background-color: $bg-gray-900;
|
||||
border-bottom: 0.5vh solid $ontime-pink;
|
||||
margin-left: -0.5em;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
|
||||
.backstageInd {
|
||||
color: $ontime-pink;
|
||||
margin-left: auto;
|
||||
margin-right: -0.5vw;
|
||||
}
|
||||
|
||||
.backstageInd::before {
|
||||
content: '*';
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import style from './TitleCard.module.css';
|
||||
import React from 'react';
|
||||
import style from './TitleCard.module.scss';
|
||||
|
||||
export default function TitleCard(props) {
|
||||
const { label, title, subtitle, presenter } = props;
|
||||
|
||||
+6
-9
@@ -1,27 +1,24 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.label {
|
||||
font-size: 1.3vw;
|
||||
color: #ff7597;
|
||||
@include card-label;
|
||||
}
|
||||
|
||||
.title,
|
||||
.subtitle,
|
||||
.presenter {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@include ellipsis;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #ddd;
|
||||
@include card-title;
|
||||
font-size: 2.5vw;
|
||||
flex: 1;
|
||||
font-weight: 600;
|
||||
|
||||
}
|
||||
|
||||
.subtitle,
|
||||
.presenter {
|
||||
color: #aaa;
|
||||
color: $subtitle-gray;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
@@ -1,4 +1,5 @@
|
||||
import style from './TitleSide.module.css';
|
||||
import React from 'react';
|
||||
import style from './TitleSide.module.scss';
|
||||
|
||||
export default function TitleSide(props) {
|
||||
const { type, label, title, subtitle, presenter } = props;
|
||||
|
||||
+5
-5
@@ -1,19 +1,19 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.label {
|
||||
font-size: 1.3vw;
|
||||
color: #ff7597;
|
||||
@include card-label;
|
||||
}
|
||||
|
||||
.nowTitle,
|
||||
.nextTitle {
|
||||
color: #ddd;
|
||||
font-weight: 600;
|
||||
@include card-title;
|
||||
}
|
||||
|
||||
.nowSubtitle,
|
||||
.nowPresenter,
|
||||
.nextSubtitle,
|
||||
.nextPresenter {
|
||||
color: #aaa;
|
||||
color: $subtitle-gray;
|
||||
}
|
||||
|
||||
.nowTitle {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user