Compare commits
49 Commits
v.0.4.3-beta
...
v1.2.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
| c576b6308e | |||
| 5802e7cd03 | |||
| 86957593bb | |||
| 9c5c7ee69a | |||
| 94a7ef6318 | |||
| c394cd5d4b | |||
| 412d230cee | |||
| 8c2d6595b0 | |||
| ca33624ffc | |||
| a73f4595a9 | |||
| f187d5edaf | |||
| 17573b9b73 | |||
| 6ac963684d | |||
| 45ac44b2e8 | |||
| 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 |
@@ -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,14 @@
|
||||
# Ignore config files
|
||||
.prettier*
|
||||
|
||||
# Ignore install stuff
|
||||
**/yarn.lock
|
||||
**/yarn-error.log
|
||||
|
||||
# Ignore test files
|
||||
**/__tests__/**
|
||||
**/**.test.js
|
||||
|
||||
# Ignore git and cache folders
|
||||
.git
|
||||
.cache
|
||||
@@ -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 |
|
Before Width: | Height: | Size: 201 KiB After Width: | Height: | Size: 285 KiB |
|
After Width: | Height: | Size: 241 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
@@ -1,103 +0,0 @@
|
||||
# This is a basic workflow to help you get started with Actions
|
||||
|
||||
name: CI
|
||||
|
||||
# Controls when the action will run.
|
||||
on:
|
||||
push:
|
||||
# Pattern matched against refs/tags
|
||||
tags:
|
||||
# Push events to every tag not containing /
|
||||
- '*'
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
# build a file for windows
|
||||
buildwin:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
CI: ''
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '14.x'
|
||||
|
||||
# install and build react
|
||||
- name: Install React dependencies
|
||||
run: yarn install
|
||||
working-directory: ./client
|
||||
- name: Build React App
|
||||
run: yarn build
|
||||
working-directory: ./client
|
||||
|
||||
# install node dependencies
|
||||
- name: Install nodejs dependencies
|
||||
run: yarn install
|
||||
working-directory: ./server/src
|
||||
|
||||
# install and build electron
|
||||
- name: Install Electron dependencies
|
||||
run: yarn install
|
||||
working-directory: ./server
|
||||
- name: Build Electron App
|
||||
run: yarn dist-win
|
||||
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 a file for mac
|
||||
# buildmac:
|
||||
# The type of runner that the job will run on
|
||||
# runs-on: macOS-latest
|
||||
# env:
|
||||
# CI: ''
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
# steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
# - uses: actions/checkout@v2
|
||||
|
||||
# - name: Use Node.js
|
||||
# uses: actions/setup-node@v1
|
||||
# with:
|
||||
# node-version: '14.x'
|
||||
|
||||
# install and build react
|
||||
# - name: Install React dependencies
|
||||
# run: yarn install
|
||||
# working-directory: ./client
|
||||
# - name: Build React App
|
||||
# run: yarn build
|
||||
# working-directory: ./client
|
||||
|
||||
# install and build electron
|
||||
# - name: Install Electron + nodejs dependencies
|
||||
# run: yarn install
|
||||
# working-directory: ./server
|
||||
# - name: Build Electron App
|
||||
# run: yarn dist-win
|
||||
# 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 }}
|
||||
@@ -0,0 +1,186 @@
|
||||
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 --frozen-lockfile
|
||||
working-directory: ./client
|
||||
|
||||
- name: React - Build project
|
||||
run: yarn build
|
||||
working-directory: ./client
|
||||
|
||||
# Node server
|
||||
- name: Server - Install dependencies
|
||||
run: yarn install --frozen-lockfile --production
|
||||
working-directory: ./server/src
|
||||
|
||||
# App
|
||||
- name: Electron - Install dependencies
|
||||
shell: bash
|
||||
run: yarn install --frozen-lockfile && 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 --frozen-lockfile
|
||||
working-directory: ./client
|
||||
|
||||
- name: React - Build project
|
||||
run: yarn build
|
||||
working-directory: ./client
|
||||
|
||||
# Node server
|
||||
- name: Server - Install dependencies
|
||||
run: yarn install --frozen-lockfile --production
|
||||
working-directory: ./server/src
|
||||
|
||||
# App
|
||||
- name: Electron - Install dependencies
|
||||
shell: bash
|
||||
run: yarn install --frozen-lockfile && yarn setdb
|
||||
working-directory: ./server
|
||||
- name: Electron - Build app
|
||||
run: yarn dist-win
|
||||
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 - Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
working-directory: ./client
|
||||
|
||||
- name: React - Build project
|
||||
run: yarn build
|
||||
working-directory: ./client
|
||||
|
||||
# Node server
|
||||
- name: Server - Install dependencies
|
||||
run: yarn install --frozen-lockfile --production
|
||||
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-linux
|
||||
working-directory: ./server
|
||||
|
||||
# Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: ./server/dist/ontime-linux.AppImage
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
publish_docker:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CI: ''
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup env
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
- 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 --frozen-lockfile --production
|
||||
working-directory: ./server/src
|
||||
|
||||
# Login to docker
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
# Prepare builder
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
# Build and push
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ontime:${{ env.RELEASE_VERSION }}
|
||||
@@ -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
|
||||
@@ -7,6 +7,7 @@ node_modules/
|
||||
|
||||
# testing
|
||||
coverage/
|
||||
*.mp4
|
||||
|
||||
# production
|
||||
build/
|
||||
@@ -23,13 +24,18 @@ 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/preloaded-db/db.json
|
||||
server/src/models/db.json
|
||||
TODO.md
|
||||
ontime-db/
|
||||
|
||||
# vscode stuff
|
||||
.vscode/*
|
||||
ontime.code-workspace
|
||||
|
||||
# webstorm stuff
|
||||
.idea/*
|
||||
@@ -0,0 +1,5 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
FROM node:14-alpine
|
||||
|
||||
WORKDIR /app/server
|
||||
|
||||
# Prepare UI
|
||||
COPY /client/build ../client/build
|
||||
|
||||
# Prepare Backend
|
||||
COPY /server/src ./
|
||||
|
||||
# Export default ports Main - OSC IN
|
||||
EXPOSE 4001/tcp 8888/udp
|
||||
ENV NODE_ENV=production
|
||||
ENV ONTIME_DATA=/server/
|
||||
|
||||
CMD ["yarn", "start:headless"]
|
||||
|
||||
# Build an run commandsN
|
||||
# docker build -t getontime/ontime .
|
||||
# docker run -p 4001:4001 -p 10.0.0.12:8888:8888/udp --mount type=bind,source="$(pwd)/ontime-db",target=/server/preloaded-db getontime/ontime
|
||||
@@ -1,101 +1,206 @@
|
||||
[](https://github.com/cpvalente/ontime/actions/workflows/ontime_cy.yml) [](https://github.com/cpvalente/ontime/actions/workflows/build.yml)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0) [](https://cpvalente.gitbook.io/ontime/)
|
||||
|
||||
## Download the latest releases here
|
||||
|
||||
Download the latest releases here
|
||||
- [Windows](https://gitreleases.dev/gh/cpvalente/ontime/latest/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>
|
||||
<a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-linux.AppImage"><img alt="Download Linux" src="https://github.com/cpvalente/ontime/blob/master/.github/linux-download.png"/></a>
|
||||
</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.
|
||||
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.
|
||||
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 ontime logo in the top right corner to select the desired view (event in the lower thirds view, where it is hidden).
|
||||
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 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 unattended 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 presenter timer view
|
||||
IP.ADDRESS:4001/preseter > 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
|
||||
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/editor
|
||||
|
||||
```
|
||||
|
||||
More documentation available [here](https://cpvalente.gitbook.io/ontime/)
|
||||
|
||||
## Feature List (in no specific order)
|
||||
|
||||
- [x] Distribute Data over network and render in the browser
|
||||
- [x] Different screen types
|
||||
- Stage Timer
|
||||
- Backstage Info
|
||||
- Public Info
|
||||
- Picture in Picture
|
||||
- Stage Timer
|
||||
- Backstage Info
|
||||
- Public Info
|
||||
- Picture in Picture
|
||||
- Studio Clock
|
||||
- [Make your own?](#make-your-own-viewer)
|
||||
- [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] 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)
|
||||
- [x] Multi platform (available on Windows, MacOS and Linux)
|
||||
- [x] [Headless run](#headless-run) (run server only, configure from a browser locally)
|
||||
|
||||
## 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] 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] 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 beginning and start times to 00:00 and 00:10 (**BAM**! 10 minute timer). You can quickly recall this with OSC as always
|
||||
- [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 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.
|
||||
## Rich APIs for workflow integrations
|
||||
|
||||
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
|
||||
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**
|
||||
|
||||
### Make your own viewer
|
||||
|
||||
Ontime broadcasts its data over websockets. This allows you to build your own viewers by leveranging
|
||||
basic knowledge of HTML + CSS + Javascript (or any other language that can run in the browser).
|
||||
|
||||
See [this repository](https://github.com/cpvalente/ontime-viewer-template) with a small template on
|
||||
how to get you started and read the docs about
|
||||
the [Websocket API](https://app.gitbook.com/s/-Mc0giSOToAhq0ROd0CR/control-and-feedback/websocket-api)
|
||||
|
||||
### Headless run️
|
||||
You can self host and run ontime in a docker image, the run command should:
|
||||
- expose the necessary ports (listen in Dockerfile)
|
||||
- mount a local file to persist your data (in the example: ````$(pwd)/local-data````)
|
||||
- the image name __getontime/ontime__
|
||||
|
||||
The docker image is in [available Docker Hub at getontime/ontime](https://hub.docker.com/r/getontime/ontime)
|
||||
```bash
|
||||
docker pull getontime/ontime
|
||||
```
|
||||
|
||||
```bash
|
||||
# Port 4001 - ontime server port
|
||||
# Port 8888 - OSC input, bound to localhost IP Address
|
||||
docker run -p 4001:4001 -p 127.0.0.1:8888:8888/udp --mount type=bind,source="$(pwd)/ontime-db",target=/server/preloaded-db getontime/ontime
|
||||
```
|
||||
|
||||
or if running from the docker compose
|
||||
|
||||
```bash
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
## 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.
|
||||
- [ ] Linux version
|
||||
- [ ] Headless version (run server only anywhere, configure from a browser locally)
|
||||
|
||||
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)
|
||||
- [ ] Companion module
|
||||
- [ ] 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
|
||||
- [ ] vMix integration
|
||||
|
||||
### For version 1
|
||||
Almost reaching a feature set that we can call v1. Before that:
|
||||
- [ ] Mac OS version
|
||||
|
||||
### 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
|
||||
|
||||
#### Style
|
||||
- [ ] App needs improvement on handling zoomed interfaces: Please ensure that windows settings have no display zoom (it is 125% by default)
|
||||
- [ ] Very long titles might cause interface to shift
|
||||
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 note your
|
||||
device and its screen resolution
|
||||
|
||||
#### 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```
|
||||
|
||||
or in Linux
|
||||
|
||||
```Could Not Display "ontime-linux.AppImage```
|
||||
|
||||
You can circumvent this by allowing the execution of the app manually.
|
||||
- In Windows: click more and select "Run Anyway"
|
||||
- in macOS: after attempting to run the installer, navigate to System Preferences -> Security & Privacy and allow the execution of the app
|
||||
- In Linux: right-click the AppImage file -> Properties -> Permissions -> select Allow Executing File as a Program
|
||||
|
||||
Long story short: Ontime app is unsigned. </br>Purchasing the certificates for both Mac and Windows
|
||||
would mean a recurrent expense 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 might have small inconsistencies
|
||||
- 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/)
|
||||
|
||||
# License
|
||||
|
||||
This project is licensed under the terms of the GNU GPL v3
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,44 +1,39 @@
|
||||
{
|
||||
"name": "ontime",
|
||||
"version": "0.1.0",
|
||||
"name": "ontime-ui",
|
||||
"version": "1.1.1",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@chakra-ui/react": "^1.7.1",
|
||||
"@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.2",
|
||||
"@chakra-ui/react": "^2.0.0-next.3",
|
||||
"@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.3",
|
||||
"framer-motion": "^6.3.3",
|
||||
"react": "^18.1.0",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-dom": "^18.1.0",
|
||||
"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",
|
||||
"react-qr-code": "^2.0.5",
|
||||
"react-query": "^3.38.0",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-table": "^7.7.0",
|
||||
"socket.io-client": "^4.5.0",
|
||||
"typeface-open-sans": "^1.1.13",
|
||||
"use-fit-text": "^2.4.0",
|
||||
"web-vitals": "^1.0.1"
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"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%",
|
||||
@@ -52,6 +47,13 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"sass": "^1.44.0"
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/react": "^13.1.1",
|
||||
"@testing-library/react-hooks": "^8.0.0",
|
||||
"@testing-library/user-event": "^14.1.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-test-renderer": "^18.1.0",
|
||||
"sass": "^1.51.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
<title>ontime</title>
|
||||
<style>
|
||||
body,
|
||||
html {
|
||||
|
||||
@@ -1,53 +1,52 @@
|
||||
import { lazy, Suspense, useCallback, 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 ErrorBoundary from 'common/components/errorBoundary/ErrorBoundary';
|
||||
import { useFetch } from './app/hooks/useFetch';
|
||||
import { ALIASES } from './app/api/apiConstants';
|
||||
import { getAliases } from './app/api/ontimeApi';
|
||||
|
||||
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 Editor = lazy(() => import('features/editors/ProtectedEditor'));
|
||||
const Table = lazy(() => import('features/table/ProtectedTable'));
|
||||
|
||||
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 SPresenter = withSocket(PresenterView);
|
||||
const SPresenterSimple = 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 FeatureWrapper = lazy(() => import('features/FeatureWrapper'));
|
||||
const EventList = lazy(() => import('features/editors/list/EventListExport'));
|
||||
const TimerControl = lazy(() => import('features/control/playback/TimerControlExport'));
|
||||
const MessageControl = lazy(() => import('features/control/message/MessageControlExport'));
|
||||
const Info = lazy(() => import('features/info/InfoExport'));
|
||||
|
||||
function App() {
|
||||
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');
|
||||
}
|
||||
@@ -65,33 +64,86 @@ function App() {
|
||||
};
|
||||
}, [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'>
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback={null}>
|
||||
<Switch>
|
||||
<Route exact path='/' component={SPresenter} />
|
||||
<Route exact path='/sm' component={SStageManager} />
|
||||
<Route exact path='/speaker' component={SPresenter} />
|
||||
<Route exact path='/presenter' component={SPresenter} />
|
||||
<Route exact path='/stage' component={SPresenter} />
|
||||
<Route exact path='/presentersimple' component={SPresenterSimple} />
|
||||
<Route exact path='/editor' component={Editor} />
|
||||
<Route exact path='/public' component={SPublic} />
|
||||
<Route exact path='/pip' component={SPip} />
|
||||
<Route exact path='/studio' component={SStudio} />
|
||||
{/* Lower cannot have fallback */}
|
||||
<Route exact path='/lower' component={SLowerThird} />
|
||||
{/* Send to default if nothing found */}
|
||||
<Route component={SPresenter} />
|
||||
</Switch>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</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={<Editor />} />
|
||||
<Route path='/cuesheet' element={<Table />} />
|
||||
<Route path='/cuelist' element={<Table />} />
|
||||
<Route path='/table' element={<Table />} />
|
||||
|
||||
{/*/!* Protected Routes - Elements *!/*/}
|
||||
<Route
|
||||
path='/eventlist'
|
||||
element={
|
||||
<FeatureWrapper>
|
||||
<EventList />
|
||||
</FeatureWrapper>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/timercontrol'
|
||||
element={
|
||||
<FeatureWrapper>
|
||||
<TimerControl />
|
||||
</FeatureWrapper>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/messagecontrol'
|
||||
element={
|
||||
<FeatureWrapper>
|
||||
<MessageControl />
|
||||
</FeatureWrapper>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/info'
|
||||
element={
|
||||
<FeatureWrapper>
|
||||
<Info />
|
||||
</FeatureWrapper>
|
||||
}
|
||||
/>
|
||||
{/* Send to default if nothing found */}
|
||||
<Route path='*' element={<STimer />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
body,
|
||||
html,
|
||||
.App {
|
||||
margin: 0px auto;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
overflow: clip;
|
||||
height: 100vh;
|
||||
@@ -23,7 +23,8 @@ option {
|
||||
|
||||
/* width */
|
||||
::-webkit-scrollbar {
|
||||
width: 0.5em;
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
@@ -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,14 +1,25 @@
|
||||
export const NODE_PORT = 4001;
|
||||
export const STATIC_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';
|
||||
|
||||
/**
|
||||
* @description finds server path given the current location, it
|
||||
* @return {*}
|
||||
*/
|
||||
const calculateServer = () => {
|
||||
return window.location.origin.replace(window.location.port, `${NODE_PORT}/`);
|
||||
if (process.env?.NODE_ENV === 'development') {
|
||||
return `http://localhost:${STATIC_PORT}`;
|
||||
}
|
||||
return window.location.origin;
|
||||
};
|
||||
|
||||
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 eventURL = `${serverURL}/${EVENT_TABLE}`;
|
||||
export const eventsURL = `${serverURL}/${EVENTS_TABLE}`;
|
||||
export const playbackURL = `${serverURL}/playback`;
|
||||
export const ontimeURL = `${serverURL}/ontime`;
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import axios from 'axios';
|
||||
import { eventURL } from './apiConstants';
|
||||
|
||||
/**
|
||||
* @description HTTP request to fetch event data
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const fetchEvent = async () => {
|
||||
const res = await axios.get(eventURL);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const postEvent = async (data) => {
|
||||
const res = await axios.post(eventURL, data);
|
||||
return res;
|
||||
};
|
||||
/**
|
||||
* @description HTTP request to mutate event data
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const postEvent = async (data) => axios.post(eventURL, data);
|
||||
|
||||
@@ -1,44 +1,53 @@
|
||||
import axios from 'axios';
|
||||
import { eventsURL } from '../api/apiConstants';
|
||||
import { eventsURL } from './apiConstants';
|
||||
|
||||
/**
|
||||
* @description HTTP request to fetch all events
|
||||
* @return {Promise}
|
||||
*/
|
||||
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;
|
||||
};
|
||||
/**
|
||||
* @description HTTP request to post new event
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const requestPost = async (data) => axios.post(eventsURL, data);
|
||||
|
||||
export const requestPut = async (data) => {
|
||||
const res = await axios.put(eventsURL, data);
|
||||
return res;
|
||||
};
|
||||
/**
|
||||
* @description HTTP request to put new event
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const requestPut = async (data) => axios.put(eventsURL, data);
|
||||
|
||||
export const requestPatch = async (data) => {
|
||||
const res = await axios.patch(eventsURL, data);
|
||||
return res;
|
||||
};
|
||||
/**
|
||||
* @description HTTP request to modify event
|
||||
* @return {Promise}
|
||||
*/
|
||||
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;
|
||||
};
|
||||
/**
|
||||
* @description HTTP request to reorder events
|
||||
* @return {Promise}
|
||||
*/
|
||||
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;
|
||||
};
|
||||
/**
|
||||
* @description HTTP request to request application of delay
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const requestApplyDelay = async (eventId) => axios.patch(`${eventsURL}/applydelay/${eventId}`);
|
||||
|
||||
export const requestDelete = async (eventId) => {
|
||||
const res = await axios.delete(eventsURL + '/' + eventId);
|
||||
return res;
|
||||
};
|
||||
/**
|
||||
* @description HTTP request to delete given event
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const requestDelete = async (eventId) => axios.delete(`${eventsURL}/${eventId}`);
|
||||
|
||||
export const requestDeleteAll = async () => {
|
||||
const res = await axios.delete(eventsURL + '/all');
|
||||
return res;
|
||||
};
|
||||
/**
|
||||
* @description HTTP request to delete all events
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const requestDeleteAll = async () => axios.delete(`${eventsURL}/all`);
|
||||
|
||||
@@ -1,42 +1,230 @@
|
||||
import axios from 'axios';
|
||||
import { ontimeURL } from './apiConstants';
|
||||
|
||||
/**
|
||||
* @description placeholder information for ontimeInfo
|
||||
* @type {{settings: {serverPort: number, version: string}, networkInterfaces: *[]}}
|
||||
*/
|
||||
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');
|
||||
/**
|
||||
* @description placeholder information for ontimeSettings
|
||||
* @type {{pinCode: null}}
|
||||
*/
|
||||
export const ontimePlaceholderSettings = {
|
||||
pinCode: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* @description placeholder information for eventSettings
|
||||
* @type {{backstageInfo: string, endMessage: string, publicInfo: string, title: string, url: string}}
|
||||
*/
|
||||
export const eventPlaceholderSettings = {
|
||||
title: '',
|
||||
url: '',
|
||||
publicInfo: '',
|
||||
backstageInfo: '',
|
||||
endMessage: '',
|
||||
};
|
||||
|
||||
/**
|
||||
* @description placeholder information for userFields
|
||||
* @type {{user1: string, user2: string, user0: string, user9: string, user7: string, user8: string, user5: string, user6: string, user3: string, user4: string}}
|
||||
*/
|
||||
export const userFieldsPlaceholder = {
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
user3: '',
|
||||
user4: '',
|
||||
user5: '',
|
||||
user6: '',
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
};
|
||||
|
||||
/**
|
||||
* @description placeholder information for oscSettings
|
||||
* @type {{targetIP: string, port: string, portOut: string, enabled: boolean}}
|
||||
*/
|
||||
export const oscPlaceholderSettings = {
|
||||
port: '',
|
||||
portOut: '',
|
||||
targetIP: '',
|
||||
enabled: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* @description placeholder information for httpSettings
|
||||
* @type {{onStart: {url: string, enabled: boolean}, onLoad: {url: string, enabled: boolean}, onPause: {url: string, enabled: boolean}, onFinish: {url: string, enabled: boolean}, onUpdate: {url: string, enabled: boolean}, onStop: {url: string, enabled: boolean}}}
|
||||
*/
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @description ontime utility variables
|
||||
* @type {[{name: string, description: string}, {name: string, description: string}, {name: string, description: string}, {name: string, description: string}, {name: string, description: string}, null, null]}
|
||||
*/
|
||||
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',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* @description HTTP request to retrieve application settings
|
||||
* @return {Promise}
|
||||
*/
|
||||
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;
|
||||
/**
|
||||
* @description HTTP request to mutate application settings
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const postSettings = async (data) => axios.post(`${ontimeURL}/settings`, data);
|
||||
|
||||
/**
|
||||
* @description HTTP request to retrieve application info
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const getInfo = async () => {
|
||||
const res = await axios.get(`${ontimeURL}/info`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description HTTP request to mutate application info
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const postInfo = async (data) => axios.post(`${ontimeURL}/info`, data);
|
||||
|
||||
/**
|
||||
* @description HTTP request to retrieve aliases
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const getAliases = async () => {
|
||||
const res = await axios.get(`${ontimeURL}/aliases`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description HTTP request to mutate aliases
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const postAliases = async (data) => axios.post(`${ontimeURL}/aliases`, data);
|
||||
|
||||
/**
|
||||
* @description HTTP request to retrieve user fields
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const getUserFields = async () => {
|
||||
const res = await axios.get(`${ontimeURL}/userfields`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description HTTP request to mutate user fields
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const postUserFields = async (data) => axios.post(`${ontimeURL}/userfields`, data);
|
||||
|
||||
/**
|
||||
* @description HTTP request to retrieve osc settings
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const getOSC = async () => {
|
||||
const res = await axios.get(`${ontimeURL}/osc`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description HTTP request to mutate osc settings
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const postOSC = async (data) => axios.post(`${ontimeURL}/osc`, data);
|
||||
|
||||
/**
|
||||
* @description HTTP request to download db
|
||||
* @return {Promise}
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||
const url = window.URL.createObjectURL(
|
||||
new Blob([response.data], { type: 'application/json' })
|
||||
);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', filename);
|
||||
@@ -45,19 +233,22 @@ export const downloadEvents = async () => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description HTTP request to upload events db
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const uploadEvents = async (file) => {
|
||||
const formData = new FormData();
|
||||
formData.append('userFile', 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));
|
||||
await axios.post(`${ontimeURL}/db`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const uploadEventsWithPath = async (filepath) => {
|
||||
await axios.post(ontimeURL + '/dbpath', { path: filepath });
|
||||
};
|
||||
/**
|
||||
* @description HTTP request to upload events
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const uploadEventsWithPath = async (filepath) => axios.post(`${ontimeURL}/dbpath`, { path: filepath });
|
||||
|
||||
@@ -1,37 +1,44 @@
|
||||
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;
|
||||
};
|
||||
/**
|
||||
* @description HTTP call to start current timer
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const getStart = async () => axios.get(`${playbackURL}/start`);
|
||||
|
||||
export const getPause = async () => {
|
||||
const res = axios.get(playbackURL + '/pause');
|
||||
return res;
|
||||
};
|
||||
/**
|
||||
* @description HTTP call to pause current timer
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const getPause = async () => axios.get(`${playbackURL}/pause`);
|
||||
|
||||
export const getRoll = async () => {
|
||||
const res = axios.get(playbackURL + '/roll');
|
||||
return res;
|
||||
};
|
||||
/**
|
||||
* @description HTTP call to start roll mode
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const getRoll = async () => axios.get(`${playbackURL}/roll`);
|
||||
|
||||
export const getPrevious = async () => {
|
||||
const res = axios.get(playbackURL + '/previous');
|
||||
return res;
|
||||
};
|
||||
/**
|
||||
* @description HTTP call to skip to previous event
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const getPrevious = async () => axios.get(`${playbackURL}/previous`);
|
||||
|
||||
export const getNext = async () => {
|
||||
const res = axios.get(playbackURL + '/next');
|
||||
return res;
|
||||
};
|
||||
/**
|
||||
* @description HTTP call to skip to next event
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const getNext = async () => axios.get(`${playbackURL}/next`);
|
||||
|
||||
export const getUnload = async () => {
|
||||
const res = axios.get(playbackURL + '/unload');
|
||||
return res;
|
||||
};
|
||||
/**
|
||||
* @description HTTP call to unload current timer
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const getUnload = async () => axios.get(`${playbackURL}/unload`);
|
||||
|
||||
export const getReload = async () => {
|
||||
const res = axios.get(playbackURL + '/reload');
|
||||
return res;
|
||||
};
|
||||
/**
|
||||
* @description HTTP call to reload current timer
|
||||
* @return {Promise}
|
||||
*/
|
||||
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,55 @@
|
||||
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;
|
||||
const previousEntry = sessionStorage.getItem('ontime-entry');
|
||||
if (previousEntry) {
|
||||
if (previousEntry === data?.pinCode) {
|
||||
setAuth(true);
|
||||
} else {
|
||||
sessionStorage.removeItem('ontime-entry')
|
||||
}
|
||||
} else if (data?.pinCode == null || data?.pinCode === '') {
|
||||
setAuth(true);
|
||||
} else {
|
||||
setAuth(false);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
/**
|
||||
* Validates a pincode
|
||||
* @return boolean - whether the pin is valid
|
||||
*/
|
||||
const validate = useCallback(
|
||||
(pin) => {
|
||||
let correct;
|
||||
if (data?.pinCode == null || data?.pinCode === '') {
|
||||
correct = true;
|
||||
} else {
|
||||
correct = pin === data?.pinCode;
|
||||
}
|
||||
if (correct) {
|
||||
sessionStorage.setItem('ontime-entry', pin);
|
||||
}
|
||||
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;
|
||||
};
|
||||
@@ -1,6 +1,11 @@
|
||||
import { useQuery } from 'react-query';
|
||||
const refetchIntervalMs = 10000;
|
||||
|
||||
/**
|
||||
* @description utility hook to simplify query config
|
||||
* @param namespace
|
||||
* @param fn
|
||||
*/
|
||||
export const useFetch = (namespace, fn) => {
|
||||
const { data, status, isError, refetch } = useQuery(namespace, fn, {
|
||||
refetchInterval: refetchIntervalMs,
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
/**
|
||||
* @description utility hook to around setInterval
|
||||
* @param callback
|
||||
* @param delay
|
||||
*/
|
||||
export const useInterval = (callback, delay) => {
|
||||
const savedCallback = useRef();
|
||||
|
||||
@@ -8,11 +13,14 @@ export const useInterval = (callback, delay) => {
|
||||
}, [callback]);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* @description function to be called
|
||||
*/
|
||||
function tick() {
|
||||
savedCallback.current();
|
||||
}
|
||||
if (delay !== null) {
|
||||
let id = setInterval(tick, delay);
|
||||
const id = setInterval(tick, delay);
|
||||
return () => clearInterval(id);
|
||||
}
|
||||
}, [delay]);
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
// Roughly from useHooks - useLocalStorage
|
||||
|
||||
/**
|
||||
* @description utility hook to handle state in local storage
|
||||
* @param key
|
||||
* @param initialValue
|
||||
*/
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @description Set value to local storage
|
||||
* @param value
|
||||
*/
|
||||
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,36 @@
|
||||
import { useMutation, useQueryClient } from 'react-query';
|
||||
import { EVENTS_TABLE } from '../api/apiConstants';
|
||||
|
||||
/**
|
||||
* @description utility hook to handle mutations in events
|
||||
* @param mutation
|
||||
*/
|
||||
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));
|
||||
@@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
import { Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/menu';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiClock } from '@react-icons/all-files/fi/FiClock';
|
||||
import { FiMinusCircle } from '@react-icons/all-files/fi/FiMinusCircle';
|
||||
import { FiPlus } from '@react-icons/all-files/fi/FiPlus';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function ActionButtons(props) {
|
||||
const { showAdd, showDelay, showBlock, actionHandler } = props;
|
||||
|
||||
const menuStyle = {
|
||||
color: '#000000',
|
||||
backgroundColor: 'rgba(255,255,255,1)',
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu isLazy lazyBehavior='unmount'>
|
||||
<Tooltip label='Add ...' delay={500}>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
aria-label='Options'
|
||||
size='xs'
|
||||
icon={<FiPlus />}
|
||||
_expanded={{ bg: 'orange.300', color: 'white' }}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
backgroundColor='orange.200'
|
||||
color='orange.500'
|
||||
/>
|
||||
</Tooltip>
|
||||
<MenuList style={menuStyle}>
|
||||
<MenuItem icon={<FiPlus />} onClick={() => actionHandler('event')} isDisabled={!showAdd}>
|
||||
Add Event after
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem icon={<FiClock />} onClick={() => actionHandler('delay')} isDisabled={!showDelay}>
|
||||
Add Delay after
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={<FiMinusCircle />}
|
||||
onClick={() => actionHandler('block')}
|
||||
isDisabled={!showBlock}
|
||||
>
|
||||
Add Block after
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
ActionButtons.propTypes = {
|
||||
showAdd: PropTypes.bool,
|
||||
showDelay: PropTypes.bool,
|
||||
showBlock: PropTypes.bool,
|
||||
actionHandler: PropTypes.func,
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiPlus } from 'react-icons/fi';
|
||||
|
||||
export default function AddIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
return (
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
icon={<FiPlus />}
|
||||
_expanded={{ bg: 'orange.300', color: 'white' }}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
backgroundColor={'orange.200'}
|
||||
color={'orange.500'}
|
||||
onClick={clickhandler}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { FiChevronsDown } from 'react-icons/fi';
|
||||
|
||||
export default function ApplyIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label='Apply delays'>
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
icon={<FiChevronsDown />}
|
||||
colorScheme='orange'
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiMinusCircle } from 'react-icons/fi';
|
||||
|
||||
export default function BlockIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
return (
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
icon={<FiMinusCircle />}
|
||||
colorScheme='purple'
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { FiChevronsUp } from 'react-icons/fi';
|
||||
|
||||
export default function CollapseBtn(props) {
|
||||
const { clickhandler } = props;
|
||||
return (
|
||||
<Tooltip label='Collapse all'>
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
icon={<FiChevronsUp />}
|
||||
colorScheme='white'
|
||||
variant='outline'
|
||||
background='#fff1'
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Button } from '@chakra-ui/button';
|
||||
import { FiTarget } from 'react-icons/fi';
|
||||
|
||||
export default function CurrentBtn(props) {
|
||||
const { clickhandler, active } = props;
|
||||
return (
|
||||
<Button
|
||||
size={props.size || 'xs'}
|
||||
leftIcon={<FiTarget />}
|
||||
colorScheme='whiteAlpha'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
>
|
||||
Goto Current
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiClock } from 'react-icons/fi';
|
||||
|
||||
export default function DelayIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
return (
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
icon={<FiClock />}
|
||||
colorScheme='yellow'
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { useState } from 'react';
|
||||
import { FiMinus } from 'react-icons/fi';
|
||||
|
||||
export default function DeleteIconBtn(props) {
|
||||
const { actionHandler, ...rest } = props;
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleClick = () => {
|
||||
setLoading(true);
|
||||
actionHandler('delete');
|
||||
};
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
icon={<FiMinus />}
|
||||
colorScheme='red'
|
||||
onClick={handleClick}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
disabled={loading}
|
||||
isLoading={loading}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
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';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function EnableBtn(props) {
|
||||
const { active, text, actionHandler, size = 'xs' } = props;
|
||||
return (
|
||||
<Button
|
||||
size={size}
|
||||
leftIcon={active ? <IoCheckmarkSharp /> : <IoCloseSharp />}
|
||||
colorScheme='blue'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={actionHandler}
|
||||
>
|
||||
{text}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
EnableBtn.propTypes = {
|
||||
active: PropTypes.bool,
|
||||
text: PropTypes.string,
|
||||
actionHandler: PropTypes.func,
|
||||
size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg']),
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { FiChevronsDown } from 'react-icons/fi';
|
||||
|
||||
export default function ExpandBtn(props) {
|
||||
const { clickhandler } = props;
|
||||
return (
|
||||
<Tooltip label='Expand all'>
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
icon={<FiChevronsDown />}
|
||||
colorScheme='white'
|
||||
variant='outline'
|
||||
background='#fff1'
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { FiTarget } from 'react-icons/fi';
|
||||
|
||||
export default function LockIconBtn(props) {
|
||||
const { clickhandler, active, ref } = props;
|
||||
return (
|
||||
<Tooltip label='Lock cursor to current'>
|
||||
<IconButton
|
||||
ref={ref}
|
||||
size={props.size || 'xs'}
|
||||
icon={<FiTarget />}
|
||||
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,18 +0,0 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiSkipForward } from 'react-icons/fi';
|
||||
|
||||
export default function NextIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
return (
|
||||
<IconButton
|
||||
icon={<FiSkipForward />}
|
||||
colorScheme='whiteAlpha'
|
||||
backgroundColor='#ffffff11'
|
||||
variant='outline'
|
||||
onClick={clickhandler}
|
||||
width={90}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +1,29 @@
|
||||
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';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
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'
|
||||
_hover={!disabled && { bg: 'orange.400' }}
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
width={120}
|
||||
disabled={disabled}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
PauseIconBtn.propTypes = {
|
||||
clickhandler: PropTypes.func,
|
||||
active: PropTypes.bool,
|
||||
disabled: PropTypes.bool
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiSkipBack } from 'react-icons/fi';
|
||||
|
||||
export default function PrevIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
return (
|
||||
<IconButton
|
||||
icon={<FiSkipBack />}
|
||||
colorScheme='whiteAlpha'
|
||||
backgroundColor='#ffffff11'
|
||||
variant='outline'
|
||||
onClick={clickhandler}
|
||||
width={90}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,22 +1,27 @@
|
||||
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';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function PublicIconBtn(props) {
|
||||
const { actionHandler, active, ...rest } = props;
|
||||
const { actionHandler, active, size = 'xs', ...rest } = props;
|
||||
return (
|
||||
<Tooltip label={active ? 'Make event private' : 'Make event public'}>
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
size={size}
|
||||
icon={<FiUsers />}
|
||||
colorScheme='blue'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={() =>
|
||||
actionHandler('update', { field: 'isPublic', value: !active })
|
||||
}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
onClick={() => actionHandler('update', { field: 'isPublic', value: !active })}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
PublicIconBtn.propTypes = {
|
||||
actionHandler: PropTypes.func,
|
||||
active: PropTypes.bool,
|
||||
size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg']),
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||
import { Button, IconButton } from '@chakra-ui/button';
|
||||
import {
|
||||
AlertDialog,
|
||||
@@ -8,25 +9,36 @@ import {
|
||||
AlertDialogOverlay,
|
||||
} from '@chakra-ui/modal';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { useRef, useState } from 'react';
|
||||
import { FiPower } from 'react-icons/fi';
|
||||
import { FiPower } from '@react-icons/all-files/fi/FiPower';
|
||||
import PropTypes from 'prop-types';
|
||||
import { LoggingContext } from '../../../app/context/LoggingContext';
|
||||
|
||||
export default function QuitIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
const { clickHandler, size = 'lg', ...rest } = props;
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { emitInfo } = useContext(LoggingContext);
|
||||
const onClose = () => setIsOpen(false);
|
||||
const cancelRef = useRef();
|
||||
|
||||
const handleShutdown = () => {
|
||||
useEffect(() => {
|
||||
if (window.process?.type === 'renderer') {
|
||||
window.ipcRenderer.on('user-request-shutdown', () => {
|
||||
emitInfo('Shutdown request')
|
||||
setIsOpen(true);
|
||||
});
|
||||
}
|
||||
}, [emitInfo]);
|
||||
|
||||
const handleShutdown = useCallback(() => {
|
||||
onClose();
|
||||
clickhandler();
|
||||
};
|
||||
clickHandler();
|
||||
}, [clickHandler]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip label='Quit Application'>
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
size={size}
|
||||
icon={<FiPower />}
|
||||
colorScheme='red'
|
||||
variant='outline'
|
||||
@@ -36,22 +48,15 @@ export default function QuitIconBtn(props) {
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
<AlertDialog
|
||||
isOpen={isOpen}
|
||||
leastDestructiveRef={cancelRef}
|
||||
onClose={onClose}
|
||||
>
|
||||
<AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}>
|
||||
<AlertDialogOverlay>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader fontSize='lg' fontWeight='bold'>
|
||||
Server Shutdown
|
||||
</AlertDialogHeader>
|
||||
|
||||
<AlertDialogBody>
|
||||
This will shutdown the program and all running servers. Are you
|
||||
sure?
|
||||
This will shutdown the program and all running servers. Are you sure?
|
||||
</AlertDialogBody>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<Button ref={cancelRef} onClick={onClose}>
|
||||
Cancel
|
||||
@@ -66,3 +71,8 @@ export default function QuitIconBtn(props) {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
QuitIconBtn.propTypes = {
|
||||
clickHandler: PropTypes.func,
|
||||
size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg']),
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiRefreshCcw } from 'react-icons/fi';
|
||||
|
||||
export default function ReloadIconButton(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
return (
|
||||
<IconButton
|
||||
icon={<FiRefreshCcw />}
|
||||
colorScheme='whiteAlpha'
|
||||
backgroundColor='#ffffff05'
|
||||
variant='outline'
|
||||
onClick={clickhandler}
|
||||
width={90}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +1,28 @@
|
||||
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';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
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}
|
||||
disabled={disabled}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
RollIconBtn.propTypes = {
|
||||
clickhandler: PropTypes.func,
|
||||
active: PropTypes.bool,
|
||||
disabled: PropTypes.bool
|
||||
}
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
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';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
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}
|
||||
disabled={disabled}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
StartIconBtn.propTypes = {
|
||||
clickhandler: PropTypes.func,
|
||||
active: PropTypes.bool,
|
||||
disabled: PropTypes.bool
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function TooltipActionBtn(props) {
|
||||
const { clickHandler, icon, color, size='xs', tooltip, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label={tooltip}>
|
||||
<IconButton
|
||||
aria-label={tooltip}
|
||||
size={size}
|
||||
icon={icon}
|
||||
onClick={clickHandler}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
TooltipActionBtn.propTypes = {
|
||||
clickHandler: PropTypes.func,
|
||||
icon: PropTypes.element,
|
||||
color: PropTypes.string,
|
||||
size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg']),
|
||||
tooltip: PropTypes.string
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function TooltipLoadingActionBtn(props) {
|
||||
const { clickHandler, icon, color, size = 'xs', tooltip, ...rest } = props;
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
setLoading(true);
|
||||
clickHandler();
|
||||
},[clickHandler, setLoading]);
|
||||
|
||||
return (
|
||||
<Tooltip label={tooltip} shouldWrapChildren={loading}>
|
||||
<IconButton
|
||||
aria-label={tooltip}
|
||||
size={size}
|
||||
icon={icon}
|
||||
onClick={handleClick}
|
||||
disabled={loading}
|
||||
isLoading={loading}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
TooltipLoadingActionBtn.propTypes = {
|
||||
clickHandler: PropTypes.func,
|
||||
icon: PropTypes.element,
|
||||
color: PropTypes.string,
|
||||
size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg']),
|
||||
tooltip: PropTypes.string,
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function TransportIconBtn(props) {
|
||||
const { clickHandler, icon, tooltip, disabled, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label={tooltip} openDelay={500} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={icon}
|
||||
colorScheme='white'
|
||||
variant='outline'
|
||||
_hover={!disabled && { bg: '#ebedf0', color: '#333' }}
|
||||
onClick={clickHandler}
|
||||
width={90}
|
||||
disabled={disabled}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
TransportIconBtn.propTypes = {
|
||||
clickHandler: PropTypes.func,
|
||||
icon: PropTypes.element,
|
||||
tooltip: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiTrash2 } from 'react-icons/fi';
|
||||
|
||||
export default function TrashIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
return (
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
icon={<FiTrash2 />}
|
||||
colorScheme='red'
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +1,27 @@
|
||||
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';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function UnloadIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
const { clickHandler, disabled, ...rest } = props;
|
||||
return (
|
||||
<IconButton
|
||||
icon={<FiXOctagon />}
|
||||
colorScheme='red'
|
||||
backgroundColor='#ff000022'
|
||||
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}
|
||||
disabled={disabled}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
UnloadIconBtn.propTypes = {
|
||||
clickHandler: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiSun } from 'react-icons/fi';
|
||||
|
||||
export default function VisibleIconBtn(props) {
|
||||
const { actionHandler, active, ...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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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,24 +1,28 @@
|
||||
import { memo } from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import { formatDisplay } from 'common/utils/dateConfig';
|
||||
import styles from './Countdown.module.css';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './Countdown.module.scss';
|
||||
|
||||
const Countdown = ({ time, small, negative, hideZeroHours }) => {
|
||||
const Countdown = ({ time, small, isNegative, hideZeroHours }) => {
|
||||
// prepare display string
|
||||
const display =
|
||||
time != null && !isNaN(time)
|
||||
? formatDisplay(time, hideZeroHours)
|
||||
: '-- : -- : --';
|
||||
time != null && !isNaN(time) ? formatDisplay(time, hideZeroHours) : '-- : -- : --';
|
||||
|
||||
const colour = negative ? '#ff7597' : '#fffffa';
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,6 +1,13 @@
|
||||
/* eslint-disable react/destructuring-assignment */
|
||||
import React from 'react';
|
||||
import { LoggingContext } from '../../../app/context/LoggingContext';
|
||||
import style from './ErrorBoundary.module.scss';
|
||||
const appVersion = require('../../../../package.json').version;
|
||||
|
||||
class ErrorBoundary extends React.Component {
|
||||
static contextType = LoggingContext;
|
||||
reportContent = '';
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { error: null, errorInfo: null };
|
||||
@@ -16,17 +23,47 @@ class ErrorBoundary extends React.Component {
|
||||
error: error,
|
||||
errorInfo: info,
|
||||
});
|
||||
// TODO: Log the error to an error reporting service
|
||||
this.logErrorToServices(error.toString(), info.componentStack);
|
||||
try {
|
||||
this.context.emitError(error.toString());
|
||||
} catch {
|
||||
console.log('Unable to emit error')
|
||||
}
|
||||
this.reportContent = `${error} ${info.componentStack}`;
|
||||
}
|
||||
|
||||
// A fake logging service.
|
||||
logErrorToServices = console.log;
|
||||
|
||||
render() {
|
||||
if (this.state.errorMessage) {
|
||||
// You can render any custom fallback UI
|
||||
return <p>:/</p>;
|
||||
return (
|
||||
<div className={style.errorContainer}>
|
||||
<div>
|
||||
<p className={style.error}>:/</p>
|
||||
<p>Something went wrong</p>
|
||||
<p
|
||||
className={style.report}
|
||||
onClick={() => {
|
||||
if (navigator.clipboard) {
|
||||
const copyContent = `ontime version ${appVersion} \n ${this.reportContent}`;
|
||||
navigator.clipboard.writeText(copyContent);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Copy error
|
||||
</p>
|
||||
<p
|
||||
className={style.report}
|
||||
onClick={() => {
|
||||
if (window.process.type === 'renderer') {
|
||||
window.ipcRenderer.send('reload');
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Reload interface
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
|
||||
@@ -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,25 +1,42 @@
|
||||
import React, { useCallback, 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
|
||||
/**
|
||||
* 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 = useCallback(
|
||||
() => (entry, val) => {
|
||||
if (val == null || timeStart == null || timeEnd == null) return true;
|
||||
if (timeStart === 0) return true;
|
||||
|
||||
if (v == null || timeStart == null || timeEnd == null) return true;
|
||||
if (timeStart === 0) return true;
|
||||
let start = timeStart;
|
||||
let end = timeEnd;
|
||||
if (entry === 'timeStart') {
|
||||
start = val;
|
||||
} else if (entry === 'timeEnd') {
|
||||
end = val;
|
||||
} else return entry === 'durationOverride';
|
||||
|
||||
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';
|
||||
|
||||
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;
|
||||
},
|
||||
[emitWarning, timeEnd, timeStart]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -29,6 +46,7 @@ export default function EventTimes(props) {
|
||||
actionHandler={actionHandler}
|
||||
time={timeStart}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<EditableTimer
|
||||
name='timeEnd'
|
||||
@@ -36,7 +54,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,71 @@
|
||||
import EditableTimer from 'common/input/EditableTimer';
|
||||
import { showWarningToast } from 'common/helpers/toastManager';
|
||||
import { stringFromMillis } from 'common/utils/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, { useCallback, 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;
|
||||
if (timeStart === 0) 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 = useCallback(
|
||||
() => (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;
|
||||
},
|
||||
[emitWarning, timeEnd, timeStart]
|
||||
);
|
||||
|
||||
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,7 @@
|
||||
import { clamp } from 'app/utils';
|
||||
import styles from './MyProgressBar.module.css';
|
||||
import React from 'react';
|
||||
import { clamp } from 'app/utils/math';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './MyProgressBar.module.scss';
|
||||
|
||||
export default function MyProgressBar(props) {
|
||||
const { now, complete, showElapsed } = props;
|
||||
@@ -21,3 +23,9 @@ export default function MyProgressBar(props) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
MyProgressBar.propTypes = {
|
||||
now: PropTypes.number,
|
||||
complete: PropTypes.number,
|
||||
showElapsed: PropTypes.bool,
|
||||
}
|
||||
|
||||
@@ -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,27 +1,49 @@
|
||||
import PropTypes from "prop-types";
|
||||
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, useCallback } from 'react';
|
||||
import navlogo from 'assets/images/logos/LOGO-72.png';
|
||||
import style from './NavLogo.module.scss';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { IoExpand } from '@react-icons/all-files/io5/IoExpand';
|
||||
|
||||
/* Styling for action buttons */
|
||||
const navButtonStyle = {
|
||||
size: 'md',
|
||||
variant: 'outline',
|
||||
colorScheme: 'whiteAlpha',
|
||||
_hover: { bg: '#ebedf0', color: '#333' },
|
||||
};
|
||||
|
||||
export default function NavLogo(props) {
|
||||
const {isHidden} = props;
|
||||
const { isHidden } = props;
|
||||
const [showNav, setShowNav] = useState(false);
|
||||
|
||||
const handleClick = () => {
|
||||
setShowNav(!showNav);
|
||||
};
|
||||
const handleClick = useCallback(() => {
|
||||
setShowNav((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
const handleKeyPress = useCallback((e) => {
|
||||
// handle held key
|
||||
if (e.repeat) return;
|
||||
// Space bar
|
||||
if (e.keyCode === 32) {
|
||||
setShowNav((s) => !s);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const toggleFullscreen = useCallback(() => {
|
||||
if (!document.fullscreenElement) {
|
||||
document.documentElement.requestFullscreen();
|
||||
} else {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// attach the event listener
|
||||
document.addEventListener('keydown', handleKeyPress);
|
||||
@@ -32,76 +54,66 @@ export default function NavLogo(props) {
|
||||
};
|
||||
}, [handleKeyPress]);
|
||||
|
||||
const baseOpacity = (isHidden) ? 0 : 0.5
|
||||
const baseOpacity = isHidden ? 0 : 0.5;
|
||||
const tabProps = {
|
||||
className: style.navItem,
|
||||
tabIndex: 0,
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: baseOpacity }}
|
||||
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 }}
|
||||
exit={{ opacity: 0, scaleY: 0, y: -50 }}
|
||||
className={showNav ? style.nav : style.navHidden}
|
||||
>
|
||||
<Link
|
||||
to='/presenter'
|
||||
className={style.navItem}
|
||||
tabIndex={1}
|
||||
onKeyDownCapture={() => <Redirect push to='/presenter' />}
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scaleX: 0 }}
|
||||
animate={{ opacity: 1, scaleX: 1 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
exit={{ opacity: 0, scaleX: 0 }}
|
||||
className={style.actions}
|
||||
>
|
||||
Presenter
|
||||
</Link>
|
||||
<Link
|
||||
to='/sm'
|
||||
className={style.navItem}
|
||||
tabIndex={2}
|
||||
onKeyDownCapture={() => <Redirect push to='/sm' />}
|
||||
<IconButton
|
||||
aria-label='Toggle Fullscreen'
|
||||
icon={<IoExpand />}
|
||||
onClick={toggleFullscreen}
|
||||
{...navButtonStyle}
|
||||
/>
|
||||
</motion.div>
|
||||
<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={style.nav}
|
||||
>
|
||||
Backstage
|
||||
</Link>
|
||||
<Link
|
||||
to='/public'
|
||||
className={style.navItem}
|
||||
tabIndex={3}
|
||||
onKeyDownCapture={() => <Redirect push to='/public' />}
|
||||
>
|
||||
Public
|
||||
</Link>
|
||||
<Link
|
||||
to='/lower'
|
||||
className={style.navItem}
|
||||
tabIndex={4}
|
||||
onKeyDownCapture={() => <Redirect push to='/lower' />}
|
||||
>
|
||||
Lower Thirds
|
||||
</Link>
|
||||
<Link
|
||||
to='/pip'
|
||||
className={style.navItem}
|
||||
tabIndex={4}
|
||||
onKeyDownCapture={() => <Redirect push to='/pip' />}
|
||||
>
|
||||
PIP
|
||||
</Link>
|
||||
<Link
|
||||
to='/studio'
|
||||
className={style.navItem}
|
||||
tabIndex={5}
|
||||
onKeyDownCapture={() => <Redirect push to='/studio' />}
|
||||
>
|
||||
Studio Clock
|
||||
</Link>
|
||||
</motion.div>
|
||||
<Link to='/timer' {...tabProps}>
|
||||
Timer
|
||||
</Link>
|
||||
<Link to='/minimal' {...tabProps}>
|
||||
Minimal Timer
|
||||
</Link>
|
||||
<Link to='/sm' {...tabProps}>
|
||||
Backstage
|
||||
</Link>
|
||||
<Link to='/public' {...tabProps}>
|
||||
Public
|
||||
</Link>
|
||||
<Link to='/lower' {...tabProps}>
|
||||
Lower Thirds
|
||||
</Link>
|
||||
<Link to='/pip' {...tabProps}>
|
||||
PIP
|
||||
</Link>
|
||||
<Link to='/studio' {...tabProps}>
|
||||
Studio Clock
|
||||
</Link>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
@@ -110,4 +122,4 @@ export default function NavLogo(props) {
|
||||
|
||||
NavLogo.propTypes = {
|
||||
isHidden: PropTypes.bool,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
$nav-color: #fff;
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.navContainer {
|
||||
position: absolute;
|
||||
@@ -9,22 +9,33 @@ $nav-color: #fff;
|
||||
align-items: flex-end;
|
||||
font-size: max(1vw, 16px);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: max(3vw, 32px);
|
||||
opacity: 0.5;
|
||||
}
|
||||
.logo {
|
||||
width: max(3vw, 32px);
|
||||
opacity: 0.5;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav {
|
||||
gap: 2vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.nav,
|
||||
.showNav {
|
||||
margin-top: 2vh;
|
||||
gap: 2vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.navItem {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
padding: 0.5vh 1vw;
|
||||
border-radius: 4px;
|
||||
color: $nav-color;
|
||||
.navItem {
|
||||
background-color: $bg-overlay;
|
||||
padding: 0.5vh 1vw;
|
||||
border-radius: 4px;
|
||||
color: $text-white;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
position: absolute;
|
||||
top: 0.5vw;
|
||||
right: 75px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
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';
|
||||
import style from './ProtectRoute.module.scss';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 />}
|
||||
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;
|
||||
}
|
||||