Compare commits

..

49 Commits

Author SHA1 Message Date
Carlos Valente c576b6308e Update build.yml 2022-06-08 22:22:03 +02:00
Carlos Valente 5802e7cd03 Update build.yml 2022-06-08 22:14:38 +02:00
Carlos Valente 86957593bb Update build.yml 2022-06-08 22:03:28 +02:00
Carlos Valente 9c5c7ee69a Docker (#146)
* docker: initial config

* docker: add entrypoint for headless run

* docker: resolve path from env variable

* docker: initial config

* docker: rename preloaded data directories

* docker: update README

* docker: prevent issue with port mappings

* docker: update README.md

* docker: add docker-compose

* docker: cleanup logs

* docker: add CI

* docker: cleanup unused

* docker: use static port in development

* refactor: isolate tasks

* docker: add demo db

* fix: download db resolves according to OS

* docker: config build

* fix: ensure db path exists

* fix: add object type to block

* docker: config build

* docker: config build

* docker: env is production

* docker: preload db

* fix: remove test dir

* docker: graceful shutdown node app

* docker: env is production

* docker: add docker-compose

* docker: remove unused mappings

* docker: version bump
2022-06-08 21:57:22 +02:00
Carlos Valente 94a7ef6318 fix/141: enable applying negative delays (#142) 2022-06-07 18:57:44 +02:00
Carlos Valente c394cd5d4b Feat/104 (#140)
* feat/104: isolate control components
* feat/104: open components in isolated window
* feat/104: update tests
2022-05-29 11:19:29 +02:00
Carlos Valente 412d230cee Ux/134 (#139)
* ui/134: highlight active messages
* ui/134: create theming template
* ui/134: version bump
2022-05-28 16:25:56 +02:00
Carlos Valente 8c2d6595b0 feat/117: add toggle fullscreen button (#138) 2022-05-28 15:57:59 +02:00
Carlos Valente ca33624ffc Update README.md
add comments on linux install
2022-05-22 10:21:43 +02:00
Carlos Valente a73f4595a9 feat/123: enable linux build (#127)
* feat/123: enable linux build
* feat/123: style: handle window width gracefully
* feat/123: update README
2022-05-22 09:53:04 +02:00
Carlos Valente f187d5edaf Fix/122 (#128)
* fix/122: register quit shortcut for macOS
* fix/122: version bump
2022-05-21 22:07:43 +02:00
Carlos Valente 17573b9b73 Fix/121 (#124)
* fix/121: database is in public directory
* fix/121: refact loading sequence
* fix/121: populate with demo db
* fix/121: ensure path exists (linux issues)
2022-05-21 13:28:34 +02:00
Carlos Valente 6ac963684d V1 (#118)
* chore: upgrade minor versions
* chore: upgrade cypress
* chore: upgrade fe dependencies
* update readme
* update osx images
* update readme
* upgrade dependencies
* upgrade test dependency
* feat: add option to input autofill
* feat: autofill to the left
* chore: update docs
* chore: cleanup ci
* version bump
* style: prevent zero height bar
* fix: prevent loosing menu
* ux: improve input, no spellcheck or autocomplete
* small ux improvements in cuesheets
* feat 111/increase maximum number of events
* fix: optimistic delete issue with filter
* refact: pincode workflow
* chore: add versioning to packages
2022-05-19 10:48:56 +02:00
Carlos Valente 45ac44b2e8 Update build.yml 2022-04-17 21:29:35 +02:00
Carlos Valente f68b5f1d74 Update build.yml 2022-04-17 20:44:41 +02:00
Carlos Valente db0eee3b9c Feat/mac (#109)
* chore: upgrade dependencies
* feat/mac: draft pipeline
* feat/mac: use public folder for transient files
* feat/mac: set app fullscreen
* feat/mac: cmd + , toggles menu
* feat/mac: update readme
* refact: easier login process
* refact prevent style issues with safari
* refact reduce css download
* style: cleanup paginator design
* style: studio clock is responsive
* style: fix spacing style issues with safari
2022-04-17 20:30:39 +02:00
Carlos Valente 20d3ddbc18 version bump 2022-04-12 19:49:20 +02:00
Carlos Valente 82d0508672 hotfix/0.8.1 prevent issue with falsy value (#107) 2022-04-12 19:46:01 +02:00
Carlos Valente dd1b9bd38c version bump
version bump from fixes in #106
2022-04-10 10:07:13 +02:00
Carlos Valente b01145ca4b Fix/102 (#106)
* fix/102 improve osx feedback on navigation
* fix/102 refactor enable / disable OSC in
2022-04-10 10:05:35 +02:00
Carlos Valente 67ddcd8c68 Feat/table (#105)
* feat/table add react-table
* feat/table add protected table route
* feat/table upgrade dependencies
* feat/table support parsing of new properties

Co-authored-by: DeepSource Bot <bot@deepsource.io>
2022-04-09 22:05:56 +02:00
DeepSource Bot 8841a02754 Add .deepsource.toml 2022-02-06 14:08:33 +00:00
Carlos Valente 1a0d7810c4 Create codeql-analysis.yml 2022-01-29 10:01:10 +01:00
Carlos Valente f2dd24b74a Feat/delays (#96)
* feat/delays allow negative delays
* feat/delays recover expand all
* feat/delays refact: icons import
* feat/delays refact: upgrade chakra
* feat/delays version bump
2022-01-23 14:32:15 +01:00
Carlos Valente 6a6f68b2f9 Feat/block improvements (#95)
* feat/89-block-improvements style: replace reload icon
* feat/block-feat/block-improvements add cursor buttons
* feat/block-improvements hover to create
* feat/block-improvements change settings in modal
* feat/block-improvements version bump
2022-01-22 23:23:31 +01:00
Carlos Valente 906a6631a7 Update README.md 2022-01-14 22:26:23 +01:00
Carlos Valente 79a24a134e Hotfix/6.0.2 (#94)
* hotfix/6.0.2 fix issue where events could not have an ID
* hotfix/6.0.2 version bump
* hotfix/6.0.2 testing
2022-01-14 22:11:33 +01:00
Carlos Valente acb5d8ae08 chore/update-deps update socket.io (#93) 2022-01-14 21:00:51 +01:00
cv 5ca8202368 version bump 2022-01-14 20:49:24 +01:00
Carlos Valente ba782d5d22 hotfix/92-osc timer is absolute (#92)
Fixes issues with approximating timers in the app views
2022-01-14 20:47:39 +01:00
Carlos Valente 48093b8651 Ux/54 help (#88)
* ux/54-help tooltips
* ux/54-help refetch after mutation
* ux/54-help broadcast event index
* ux/54-help prevent multiple keypresses when key held
* ux/54-help fat clock
* ux/54-help progress bar user defined
* ux/54-help onAir is button
* ux/54-help OSC: add missing presenter message
* ux/54-help OSC: enable / disable OSC
* ux/54-help handle timezone in excel date import
* ux/54-help tray left click to show app
* ux/54-help create queriable endpoint
* ux/54-help fix memoisation in lower thirds
* ux/54-help revise icons
* ux/54-help clarify text
* ux/54-help forgiving text parsing
* ux/54-help add smart keywords
* ux/54-help version bump
2022-01-12 22:41:12 +01:00
Carlos Valente c3f18feaae Fix/82 import (#83)
* fix/82-import fix import extension filter
* fix/82-import version bump
2022-01-05 22:15:21 +01:00
Carlos Valente 2fa397548a Feat/navigate (#81)
* feat/navigate upgrade react router
* feat/navigate migrate to react router 6
* feat/navigate style modal
* feat/navigate create endpoints for app settings
* feat/navigate protect editor with pin
* feat/navigate apply dynamic routing draft
* feat/navigate upgrade relevant packages
* feat/navigate restructure directory and add tests
* feat/navigate test aliases validation
* feat/navigate validate aliases before sending
* feat/navigate create data endpoint
* feat/navigate config: prettier
* feat/navigate invalidate empty strings
* feat/navigate create endpoints
* feat/navigate parse on import
* feat/navigate navigate to alias
* feat/navigate user help and sample data
* feat/navigate refact aliases modal
* feat/navigate refact settings style
* feat/navigate update sample db
* feat/navigate link is relative to hostname
* feat/navigate navigate to first match
* feat/navigate update readme and version bump
* feat/navigate config: create shared module
* feat/navigate config: cheat module install
* feat/navigate fix tests
* feat/navigate run tests in pull request
* Update ontime_cy.yml
2022-01-04 22:12:59 +01:00
xztraz dd43d5e20a added studio clock (#80) 2022-01-03 22:24:24 +01:00
Carlos Valente 9fc154955a Feat/62 logger (#79)
* feat/62-logger request log data in app
* feat/62-logger refact osc integration
* feat/62-logger feedback on osc
* feat/62-logger refact broadcast on triggers
* feat/62-logger log triggers
* feat/62-logger add link to studio
* feat/62-logger replace toasts with logger context
* feat/62-logger style improvements
* feat/62-logger refactor code duplications
* feat/62-logger cleanup and version bump
2021-12-25 19:39:40 +01:00
Carlos Valente 160ccabebc Config/eslint (#78)
* config/eslint: upgrade eslint config
* config/eslint: version bump
2021-12-22 22:22:21 +01:00
Carlos Valente 4974898050 version bump 2021-12-22 18:19:35 +01:00
Carlos Valente ac0d5832b6 event cucle (#76)
* install sass
* refact: integration settings
- OSC in its own HTTP endpoint
- OSC settings have own object in db
* refact: simplify event cycle
* refact: restructure external triggers http
* refact: restructure external triggers osc+socket
* refact: restructure data updates
* feat: osc integration class
* IO improvements
- timer uses osc integration
- create trigger handler to manage external triggers
* refact: refract state machine update
* Integration: simple HTTP Client
* Integration: http options in datamodel
* Integration: call http send on life cycle
* feat/62-logging: fix issue #71
2021-12-22 18:17:48 +01:00
Carlos Valente 2b2bafa8c6 add mock object (#75) 2021-12-19 16:36:11 +01:00
Carlos Valente fad34eeab2 add mock object (#74) 2021-12-19 14:30:14 +01:00
Carlos Valente c12bca05aa Chore/tests (#73)
* chore/tests: install testing libraries
* chore/tests: upgrade dependencies
* chore/tests: configure linters
* chore/tests: add basic component tests
* chore/tests: version bump
2021-12-19 14:22:02 +01:00
Carlos Valente 1e4206fcb2 Refract/updatecritical (#69) 2021-12-18 14:57:07 +01:00
Carlos Valente 2751ed3d33 fix/58-countdown
correct issues on countdown clock (#68)
2021-12-16 22:13:41 +01:00
Carlos Valente cd911aaaa2 Fix/58 roll issues (#67)
* fix/58-roll-issues: handle edge cases
2021-12-15 22:31:34 +01:00
cv 1321aa555e Revert "hotfix: add missing font"
This reverts commit 28c9cb0864.
2021-12-15 20:48:43 +01:00
cv 12e4eaef67 Merge branch 'master' of https://github.com/cpvalente/ontime 2021-12-15 20:47:07 +01:00
cv 28c9cb0864 hotfix: add missing font 2021-12-15 20:47:05 +01:00
Carlos Valente afdfae9073 Hotfix/4.0.3 (#66)
* hotfix 4.0.3: style tweaks
* hotfix 4.0.3: replace font
2021-12-15 20:43:21 +01:00
Carlos Valente 0736ce93c6 Hotfix/4.0.3 (#65)
* hotfix 4.0.3: style tweaks
* hotfix 4.0.3: replace font
2021-12-14 23:19:38 +01:00
311 changed files with 24203 additions and 16685 deletions
+18
View File
@@ -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"]
+14
View File
@@ -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
+34
View File
@@ -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"
}
]
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

After

Width:  |  Height:  |  Size: 285 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

-103
View File
@@ -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 }}
+186
View File
@@ -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 }}
+70
View File
@@ -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
+53
View File
@@ -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
+11 -5
View File
@@ -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/*
+5
View File
@@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
+50
View File
@@ -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>
+8
View File
@@ -0,0 +1,8 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"jsxSingleQuote": true,
"printWidth": 100
}
+20
View File
@@ -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
+148 -43
View File
@@ -1,101 +1,206 @@
[![ontime_test_CI](https://github.com/cpvalente/ontime/actions/workflows/ontime_cy.yml/badge.svg)](https://github.com/cpvalente/ontime/actions/workflows/ontime_cy.yml) [![Ontime build](https://github.com/cpvalente/ontime/actions/workflows/build.yml/badge.svg)](https://github.com/cpvalente/ontime/actions/workflows/build.yml)
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-green.svg)](https://www.gnu.org/licenses/gpl-3.0) [![Documentation in Gitbook](https://badges.aleen42.com/src/gitbook_2.svg)](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.
![App Window](https://github.com/cpvalente/ontime/blob/master/.github/01_app.png)
![App Window](https://github.com/cpvalente/ontime/blob/master/.github/app.jpg)
![Views](https://github.com/cpvalente/ontime/blob/master/.github/02_screentypes.png)
## 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
+82
View File
@@ -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
}
]
}
}
+30 -28
View File
@@ -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"
}
}
+1 -1
View File
@@ -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 {
+108 -56
View File
@@ -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>
);
}
+3 -2
View File
@@ -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 */
-8
View File
@@ -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();
});
+9
View File
@@ -0,0 +1,9 @@
import {QueryClient} from "react-query";
export const queryClientMock = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
+17 -6
View File
@@ -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`;
+9 -4
View File
@@ -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);
+40 -31
View File
@@ -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`);
+217 -26
View File
@@ -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 });
+36 -29
View File
@@ -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`);
+16
View File
@@ -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' }
];
+55
View File
@@ -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>
);
};
+63
View File
@@ -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>
);
};
+108
View File
@@ -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>
);
};
-60
View File
@@ -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));
});
-34
View File
@@ -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;
+13
View File
@@ -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;
};
+5
View File
@@ -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,
+9 -1
View File
@@ -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]);
+37
View File
@@ -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];
}
+36
View File
@@ -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]);
},
});
}
-38
View File
@@ -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;
};
+1 -1
View File
@@ -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: {
-7
View File
@@ -1,7 +0,0 @@
export const userConfig = {
timerColorOnPause: '#555',
timerColorOnRunning: '#FFF',
timerColorOnMessage: '#CCC',
timerColorOnTimeOver: '#F00',
overTimeText: '',
}
-2
View File
@@ -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)
));
});
+29
View File
@@ -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;
};
+9
View File
@@ -0,0 +1,9 @@
/**
* Clamps a value between a min and a max
* @param {number} num - Value to clamp
* @param {number} min - min value
* @param {number} max - max value
* @returns {number}
*/
export const clamp = (num, min, max) =>
Math.max(Math.min(num, Math.max(min, max)), Math.min(min, max));
Binary file not shown.
Binary file not shown.
@@ -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;
+81 -69
View File
@@ -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;
}

Some files were not shown because too many files have changed in this diff Show More