Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 723b1cfa48 | |||
| f119fb4529 | |||
| 562cb769c5 | |||
| d86afee477 | |||
| 70436059d8 | |||
| fc0cbf6134 | |||
| 3128f5a195 | |||
| 94b03312a6 | |||
| b7e758bf24 | |||
| e40d750673 | |||
| b384284175 | |||
| 0e450cb6cb | |||
| 0651777055 | |||
| a8ce6f3fc1 | |||
| b80ae543a4 | |||
| 9b5d536378 | |||
| 05be0c1e95 | |||
| 18f3f3643b | |||
| a757058511 | |||
| a20909b882 | |||
| 91a0d975b3 | |||
| 97e48fc56e | |||
| 5c3e9e6a2e | |||
| 8ece6b141c | |||
| 3edb534e90 | |||
| a9ee0c3f1d | |||
| f4c5a2bf87 | |||
| dcc534f92b | |||
| 2ee4986dcf | |||
| c1de3190e9 | |||
| 1f001cf188 | |||
| 323d365f1a | |||
| dda92ba3a8 | |||
| c576b6308e | |||
| 5802e7cd03 | |||
| 86957593bb |
|
After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 3.4 KiB |
@@ -10,6 +10,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build_mac:
|
build_mac:
|
||||||
runs-on: macOS-latest
|
runs-on: macOS-latest
|
||||||
|
timeout-minutes: 20
|
||||||
env:
|
env:
|
||||||
CI: ''
|
CI: ''
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@ jobs:
|
|||||||
|
|
||||||
# React
|
# React
|
||||||
- name: React - Install dependencies
|
- name: React - Install dependencies
|
||||||
run: yarn install --frozen-lockfile
|
run: yarn install --frozen-lockfile --network-timeout 300000
|
||||||
working-directory: ./client
|
working-directory: ./client
|
||||||
|
|
||||||
- name: React - Build project
|
- name: React - Build project
|
||||||
@@ -31,13 +32,13 @@ jobs:
|
|||||||
|
|
||||||
# Node server
|
# Node server
|
||||||
- name: Server - Install dependencies
|
- name: Server - Install dependencies
|
||||||
run: yarn install --frozen-lockfile --production
|
run: yarn install --frozen-lockfile --production --network-timeout 300000
|
||||||
working-directory: ./server/src
|
working-directory: ./server/src
|
||||||
|
|
||||||
# App
|
# App
|
||||||
- name: Electron - Install dependencies
|
- name: Electron - Install dependencies
|
||||||
shell: bash
|
shell: bash
|
||||||
run: yarn install --frozen-lockfile && yarn setdb
|
run: yarn install --frozen-lockfile --network-timeout 300000 && yarn setdb
|
||||||
working-directory: ./server
|
working-directory: ./server
|
||||||
- name: Electron - Build app
|
- name: Electron - Build app
|
||||||
run: yarn dist-mac
|
run: yarn dist-mac
|
||||||
@@ -53,6 +54,7 @@ jobs:
|
|||||||
|
|
||||||
build_win:
|
build_win:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
|
timeout-minutes: 20
|
||||||
env:
|
env:
|
||||||
CI: ''
|
CI: ''
|
||||||
|
|
||||||
@@ -65,7 +67,7 @@ jobs:
|
|||||||
|
|
||||||
# React
|
# React
|
||||||
- name: React - Install dependencies
|
- name: React - Install dependencies
|
||||||
run: yarn install --frozen-lockfile
|
run: yarn install --frozen-lockfile --network-timeout 300000
|
||||||
working-directory: ./client
|
working-directory: ./client
|
||||||
|
|
||||||
- name: React - Build project
|
- name: React - Build project
|
||||||
@@ -74,13 +76,13 @@ jobs:
|
|||||||
|
|
||||||
# Node server
|
# Node server
|
||||||
- name: Server - Install dependencies
|
- name: Server - Install dependencies
|
||||||
run: yarn install --frozen-lockfile --production
|
run: yarn install --frozen-lockfile --production --network-timeout 300000
|
||||||
working-directory: ./server/src
|
working-directory: ./server/src
|
||||||
|
|
||||||
# App
|
# App
|
||||||
- name: Electron - Install dependencies
|
- name: Electron - Install dependencies
|
||||||
shell: bash
|
shell: bash
|
||||||
run: yarn install --frozen-lockfile && yarn setdb
|
run: yarn install --frozen-lockfile --network-timeout 300000 && yarn setdb
|
||||||
working-directory: ./server
|
working-directory: ./server
|
||||||
- name: Electron - Build app
|
- name: Electron - Build app
|
||||||
run: yarn dist-win
|
run: yarn dist-win
|
||||||
@@ -96,6 +98,7 @@ jobs:
|
|||||||
|
|
||||||
build_linux:
|
build_linux:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
env:
|
env:
|
||||||
CI: ''
|
CI: ''
|
||||||
|
|
||||||
@@ -108,7 +111,7 @@ jobs:
|
|||||||
|
|
||||||
# React
|
# React
|
||||||
- name: React - Install dependencies
|
- name: React - Install dependencies
|
||||||
run: yarn install --frozen-lockfile
|
run: yarn install --frozen-lockfile --network-timeout 300000
|
||||||
working-directory: ./client
|
working-directory: ./client
|
||||||
|
|
||||||
- name: React - Build project
|
- name: React - Build project
|
||||||
@@ -117,13 +120,13 @@ jobs:
|
|||||||
|
|
||||||
# Node server
|
# Node server
|
||||||
- name: Server - Install dependencies
|
- name: Server - Install dependencies
|
||||||
run: yarn install --frozen-lockfile --production
|
run: yarn install --frozen-lockfile --production --network-timeout 300000
|
||||||
working-directory: ./server/src
|
working-directory: ./server/src
|
||||||
|
|
||||||
# App
|
# App
|
||||||
- name: Electron - Install dependencies
|
- name: Electron - Install dependencies
|
||||||
shell: bash
|
shell: bash
|
||||||
run: yarn install && yarn setdb
|
run: yarn install --frozen-lockfile --network-timeout 300000 && yarn setdb
|
||||||
working-directory: ./server
|
working-directory: ./server
|
||||||
- name: Electron - Build app
|
- name: Electron - Build app
|
||||||
run: yarn dist-linux
|
run: yarn dist-linux
|
||||||
@@ -139,6 +142,7 @@ jobs:
|
|||||||
|
|
||||||
publish_docker:
|
publish_docker:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
env:
|
env:
|
||||||
CI: ''
|
CI: ''
|
||||||
|
|
||||||
@@ -153,7 +157,7 @@ jobs:
|
|||||||
|
|
||||||
# React
|
# React
|
||||||
- name: React - Install dependencies
|
- name: React - Install dependencies
|
||||||
run: yarn install
|
run: yarn install --network-timeout 300000
|
||||||
working-directory: ./client
|
working-directory: ./client
|
||||||
|
|
||||||
- name: React - Build project
|
- name: React - Build project
|
||||||
@@ -162,7 +166,7 @@ jobs:
|
|||||||
|
|
||||||
# Node server
|
# Node server
|
||||||
- name: Server - Install dependencies
|
- name: Server - Install dependencies
|
||||||
run: yarn install --frozen-lockfile --production
|
run: yarn install --frozen-lockfile --production --network-timeout 300000
|
||||||
working-directory: ./server/src
|
working-directory: ./server/src
|
||||||
|
|
||||||
# Login to docker
|
# Login to docker
|
||||||
@@ -171,14 +175,16 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
# Prepare builder
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
# Build and push
|
# Build and push
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
working-directory: ./
|
|
||||||
id: docker_build
|
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v2
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ontime:${{ env.RELEASE_VERSION }}
|
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ontime:${{ env.RELEASE_VERSION }} , ${{ secrets.DOCKERHUB_USERNAME }}/ontime:latest
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
env:
|
env:
|
||||||
CI: ''
|
CI: ''
|
||||||
|
|
||||||
@@ -50,4 +51,4 @@ jobs:
|
|||||||
uses: cypress-io/github-action@v2
|
uses: cypress-io/github-action@v2
|
||||||
with:
|
with:
|
||||||
working-directory: ./server
|
working-directory: ./server
|
||||||
start: yarn cypress
|
start: yarn cypress
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ server/src/preloaded-db/db.json
|
|||||||
server/src/models/db.json
|
server/src/models/db.json
|
||||||
TODO.md
|
TODO.md
|
||||||
ontime-db/
|
ontime-db/
|
||||||
|
ontime-external/
|
||||||
|
|
||||||
# vscode stuff
|
# vscode stuff
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
|||||||
@@ -46,5 +46,6 @@
|
|||||||
</list>
|
</list>
|
||||||
</option>
|
</option>
|
||||||
</inspection_tool>
|
</inspection_tool>
|
||||||
|
<inspection_tool class="Stylelint" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
</profile>
|
</profile>
|
||||||
</component>
|
</component>
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
<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-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-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>
|
<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>
|
||||||
|
<a href="https://hub.docker.com/r/getontime/ontime"><img alt="Get from Dockerhub" src="https://github.com/cpvalente/ontime/blob/master/.github/dockerhub.png"/></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
# Ontime
|
# Ontime
|
||||||
@@ -65,13 +66,14 @@ More documentation available [here](https://cpvalente.gitbook.io/ontime/)
|
|||||||
- Public Info
|
- Public Info
|
||||||
- Picture in Picture
|
- Picture in Picture
|
||||||
- Studio Clock
|
- Studio Clock
|
||||||
- [Make your own?](#make-your-own-viewer)
|
- [Make your own?](#make-your-own-viewer)
|
||||||
- [x] Configurable realtime Lower Thirds
|
- [x] Configurable realtime Lower Thirds
|
||||||
- [x] Cuesheets with additional custom fields
|
- [x] Cuesheets with additional custom fields
|
||||||
- [x] Send live messages to different screen types
|
- [x] Send live messages to different screen types
|
||||||
- [x] Ability to differentiate between backstage and public data
|
- [x] Ability to differentiate between backstage and public data
|
||||||
- [x] Manage delays workflow
|
- [x] Manage delays workflow
|
||||||
- [x] Open Sound Control (OSC) Control and Feedback
|
- [x] Open Sound Control (OSC) Control and Feedback
|
||||||
|
- [x] Integrate with hardware using Companion or one of the APIs
|
||||||
- [x] Roll mode: run independently using the system clock
|
- [x] Roll mode: run independently using the system clock
|
||||||
- [x] Import event list from Excel
|
- [x] Import event list from Excel
|
||||||
- [x] URL Aliases (define configurable aliases to ease onsite setup)
|
- [x] URL Aliases (define configurable aliases to ease onsite setup)
|
||||||
@@ -80,6 +82,8 @@ More documentation available [here](https://cpvalente.gitbook.io/ontime/)
|
|||||||
same as app)
|
same as app)
|
||||||
- [x] Multi platform (available on Windows, MacOS and Linux)
|
- [x] Multi platform (available on Windows, MacOS and Linux)
|
||||||
- [x] [Headless run](#headless-run) (run server only, configure from a browser locally)
|
- [x] [Headless run](#headless-run) (run server only, configure from a browser locally)
|
||||||
|
- [x] [Countdown to anything!](https://cpvalente.gitbook.io/ontime/views/countdown): ability to have
|
||||||
|
a countdown to any scheduled event
|
||||||
|
|
||||||
## Unopinionated
|
## Unopinionated
|
||||||
|
|
||||||
@@ -106,7 +110,9 @@ Taking advantage of the integrations in Ontime, we currently use Ontime with:
|
|||||||
webmodule
|
webmodule
|
||||||
- `OBS`: **render views** using the Browser Module
|
- `OBS`: **render views** using the Browser Module
|
||||||
- `QLab`: trigger ontime using **OSC API**
|
- `QLab`: trigger ontime using **OSC API**
|
||||||
- `Companion`: trigger ontime and manipulate timer using **OSC API**
|
- `Companion`: Ontime has a **companion module**. Issue report and feature requests should be done
|
||||||
|
in
|
||||||
|
the [repository getontime/ontime](https://github.com/bitfocus/companion-module-getontime-ontime)
|
||||||
|
|
||||||
### Make your own viewer
|
### Make your own viewer
|
||||||
|
|
||||||
@@ -118,12 +124,16 @@ how to get you started and read the docs about
|
|||||||
the [Websocket API](https://app.gitbook.com/s/-Mc0giSOToAhq0ROd0CR/control-and-feedback/websocket-api)
|
the [Websocket API](https://app.gitbook.com/s/-Mc0giSOToAhq0ROd0CR/control-and-feedback/websocket-api)
|
||||||
|
|
||||||
### Headless run️
|
### Headless run️
|
||||||
|
|
||||||
You can self host and run ontime in a docker image, the run command should:
|
You can self host and run ontime in a docker image, the run command should:
|
||||||
|
|
||||||
- expose the necessary ports (listen in Dockerfile)
|
- expose the necessary ports (listen in Dockerfile)
|
||||||
- mount a local file to persist your data (in the example: ````$(pwd)/local-data````)
|
- mount a local file to persist your data (in the example: ````$(pwd)/local-data````)
|
||||||
- the image name __getontime/ontime__
|
- the image name __getontime/ontime__
|
||||||
|
|
||||||
The docker image is in [available Docker Hub at getontime/ontime](https://hub.docker.com/r/getontime/ontime)
|
The docker image is
|
||||||
|
in [available Docker Hub at getontime/ontime](https://hub.docker.com/r/getontime/ontime)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker pull getontime/ontime
|
docker pull getontime/ontime
|
||||||
```
|
```
|
||||||
@@ -148,19 +158,15 @@ There are several features planned in the roadmap. These will be implemented in
|
|||||||
friendly order unless there is user demand to bump any of them.
|
friendly order unless there is user demand to bump any of them.
|
||||||
|
|
||||||
- [ ] HTTP Server (vMix integration)
|
- [ ] HTTP Server (vMix integration)
|
||||||
- [ ] Improvement with event component design
|
- [ ] Improvements in event interface
|
||||||
- [ ] New playback mode
|
- [ ] Moderator view
|
||||||
for [cumulative time keeping](https://github.com/cpvalente/ontime/issues/100)
|
- [ ] New playback mode for [cumulative time keeping](https://github.com/cpvalente/ontime/issues/100)
|
||||||
- [ ] Companion module
|
|
||||||
- [ ] Lower Third Manager
|
- [ ] Lower Third Manager
|
||||||
- [ ] Note only event
|
|
||||||
- [ ] Reach Schedule: way to speedup timer to meet a deadline
|
- [ ] Reach Schedule: way to speedup timer to meet a deadline
|
||||||
|
|
||||||
### Issues
|
### Issues
|
||||||
|
|
||||||
The app is still in pre-release and there are a few issues, mainly concerning responsiveness in
|
If you come across any bugs, [please open an issue]((https://github.com/cpvalente/ontime/issues/new)). Usually bugs get fixed pretty quickly when reported
|
||||||
different screens. If you run into problems, please open an issue with a screenshot and note your
|
|
||||||
device and its screen resolution
|
|
||||||
|
|
||||||
#### Unsigned App
|
#### Unsigned App
|
||||||
|
|
||||||
@@ -176,17 +182,19 @@ or in Linux
|
|||||||
|
|
||||||
```Could Not Display "ontime-linux.AppImage```
|
```Could Not Display "ontime-linux.AppImage```
|
||||||
|
|
||||||
You can circumvent this by allowing the execution of the app manually.
|
You can circumvent this by allowing the execution of the app manually.
|
||||||
|
|
||||||
- In Windows: click more and select "Run Anyway"
|
- 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 macOS: after attempting to run the installer, navigate to System Preferences -> Security &
|
||||||
- In Linux: right-click the AppImage file -> Properties -> Permissions -> select Allow Executing File as a Program
|
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
|
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
|
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,
|
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)
|
please [open an issue, so we can discuss it](https://github.com/cpvalente/ontime/issues/new)
|
||||||
|
|
||||||
|
|
||||||
#### Safari
|
#### Safari
|
||||||
|
|
||||||
There are some issues with Safari versions lower than 13:
|
There are some issues with Safari versions lower than 13:
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
build
|
||||||
@@ -1,16 +1,25 @@
|
|||||||
{
|
{
|
||||||
|
"settings": {
|
||||||
|
"react": {
|
||||||
|
"version": "detect"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
"extends": [
|
"extends": [
|
||||||
"react-app",
|
"eslint:recommended",
|
||||||
"react-app/jest",
|
"plugin:react/recommended",
|
||||||
"plugin:react/recommended"
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"eslint-config-prettier"
|
||||||
],
|
],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"react",
|
"react",
|
||||||
"testing-library",
|
"testing-library",
|
||||||
"jest"
|
"simple-import-sort"
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"jest/no-mocks-import": "warn",
|
|
||||||
"no-useless-concat": "warn",
|
"no-useless-concat": "warn",
|
||||||
"prefer-template": "warn",
|
"prefer-template": "warn",
|
||||||
"react/jsx-no-bind": [
|
"react/jsx-no-bind": [
|
||||||
@@ -41,6 +50,8 @@
|
|||||||
"react/no-deprecated": [
|
"react/no-deprecated": [
|
||||||
"error"
|
"error"
|
||||||
],
|
],
|
||||||
|
"react/jsx-uses-react": "off",
|
||||||
|
"react/react-in-jsx-scope": "off",
|
||||||
"react/jsx-no-undef": "error",
|
"react/jsx-no-undef": "error",
|
||||||
"react/jsx-closing-tag-location": "warn",
|
"react/jsx-closing-tag-location": "warn",
|
||||||
"react/jsx-no-useless-fragment": "warn",
|
"react/jsx-no-useless-fragment": "warn",
|
||||||
@@ -77,6 +88,56 @@
|
|||||||
"customValidators": [],
|
"customValidators": [],
|
||||||
"skipUndeclared": true
|
"skipUndeclared": true
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
}
|
"simple-import-sort/imports": "warn",
|
||||||
|
"simple-import-sort/exports": "warn"
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
// override "simple-import-sort" config
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"*.js",
|
||||||
|
"*.jsx",
|
||||||
|
"*.ts",
|
||||||
|
"*.tsx"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"simple-import-sort/imports": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"groups": [
|
||||||
|
// Packages `react` related packages come first.
|
||||||
|
[
|
||||||
|
"^react",
|
||||||
|
"^@?\\w"
|
||||||
|
],
|
||||||
|
// Internal packages.
|
||||||
|
[
|
||||||
|
"^(@|components)(/.*|$)"
|
||||||
|
],
|
||||||
|
// Side effect imports.
|
||||||
|
[
|
||||||
|
"^\\u0000"
|
||||||
|
],
|
||||||
|
// Parent imports. Put `..` last.
|
||||||
|
[
|
||||||
|
"^\\.\\.(?!/?$)",
|
||||||
|
"^\\.\\./?$"
|
||||||
|
],
|
||||||
|
// Other relative imports. Put same-folder imports and `.` last.
|
||||||
|
[
|
||||||
|
"^\\./(?=.*/)(?!/?$)",
|
||||||
|
"^\\.(?!/?$)",
|
||||||
|
"^\\./?$"
|
||||||
|
],
|
||||||
|
// Style imports.
|
||||||
|
[
|
||||||
|
"^.+\\.?(css)$"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"stylelint-config-standard-scss",
|
||||||
|
"stylelint-config-prettier"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang='en'>
|
||||||
|
<head>
|
||||||
|
<meta charset='utf-8' />
|
||||||
|
<link rel='icon' href='/favicon.ico' />
|
||||||
|
<meta name='viewport' content='width=device-width, initial-scale=1' />
|
||||||
|
<meta name='theme-color' content='#121212' />
|
||||||
|
<meta name='ontime' content='ontime - event timer manager' />
|
||||||
|
<link rel='apple-touch-icon' href='/logo192.png' />
|
||||||
|
<link
|
||||||
|
rel='icon'
|
||||||
|
type='image/png'
|
||||||
|
sizes='32x32'
|
||||||
|
href='/favicon-32x32.png'
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel='icon'
|
||||||
|
type='image/png'
|
||||||
|
sizes='16x16'
|
||||||
|
href='/favicon-16x16.png'
|
||||||
|
/>
|
||||||
|
<link rel='manifest' href='/site.webmanifest' />
|
||||||
|
<link rel='manifest' href='/manifest.json' />
|
||||||
|
<title>ontime</title>
|
||||||
|
<style>
|
||||||
|
body,
|
||||||
|
html {
|
||||||
|
background-color: rgba(0, 0, 0, 0) !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id='root'></div>
|
||||||
|
<script type='module' src='/src/index.jsx'></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"baseUrl": "src"
|
|
||||||
},
|
|
||||||
"include": ["src"]
|
|
||||||
}
|
|
||||||
@@ -1,38 +1,41 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime-ui",
|
"name": "ontime-ui",
|
||||||
"version": "1.1.1",
|
"version": "1.8.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/react": "^2.0.0-next.3",
|
"@chakra-ui/react": "^2.3.2",
|
||||||
"@dnd-kit/core": "^5.0.3",
|
"@dnd-kit/core": "^6.0.5",
|
||||||
"@dnd-kit/sortable": "^6.0.1",
|
"@dnd-kit/sortable": "^7.0.1",
|
||||||
"@dnd-kit/utilities": "^3.1.0",
|
"@dnd-kit/utilities": "^3.2.0",
|
||||||
"@emotion/react": "^11.7.1",
|
"@emotion/react": "^11.10.4",
|
||||||
"@emotion/styled": "^11.6.0",
|
"@emotion/styled": "^11.10.4",
|
||||||
"@react-icons/all-files": "^4.1.0",
|
"@react-icons/all-files": "^4.1.0",
|
||||||
|
"@tanstack/react-query": "^4.1.3",
|
||||||
|
"@tanstack/react-query-devtools": "^4.0.10",
|
||||||
"autosize": "^5.0.1",
|
"autosize": "^5.0.1",
|
||||||
"axios": "^0.25.0",
|
"axios": "^0.27.2",
|
||||||
"color": "^4.2.3",
|
"color": "^4.2.3",
|
||||||
"framer-motion": "^6.3.3",
|
"framer-motion": "^7.3.2",
|
||||||
|
"jotai": "^1.7.8",
|
||||||
|
"luxon": "^3.0.1",
|
||||||
"react": "^18.1.0",
|
"react": "^18.1.0",
|
||||||
"react-beautiful-dnd": "^13.1.0",
|
"react-beautiful-dnd": "^13.1.0",
|
||||||
"react-dom": "^18.1.0",
|
"react-dom": "^18.1.0",
|
||||||
"react-fast-compare": "^3.2.0",
|
"react-fast-compare": "^3.2.0",
|
||||||
"react-qr-code": "^2.0.5",
|
"react-qr-code": "^2.0.5",
|
||||||
"react-query": "^3.38.0",
|
|
||||||
"react-router-dom": "^6.3.0",
|
"react-router-dom": "^6.3.0",
|
||||||
"react-table": "^7.7.0",
|
"react-table": "^7.7.0",
|
||||||
"socket.io-client": "^4.5.0",
|
"socket.io-client": "^4.5.1",
|
||||||
"typeface-open-sans": "^1.1.13",
|
"typeface-open-sans": "^1.1.13",
|
||||||
"use-fit-text": "^2.4.0",
|
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "set BROWSER=none&&react-scripts start",
|
"start": "set BROWSER=none&&vite",
|
||||||
"build": "react-scripts build",
|
"build": "vite build",
|
||||||
"test": "react-scripts test",
|
"lint": "eslint .",
|
||||||
"test:pipeline": "react-scripts test --watchAll=false --runInBand --detectOpenHandles --forceExit ",
|
"stylelint": "npx stylelint \"**/*.scss\"\n",
|
||||||
"eject": "react-scripts eject"
|
"test": "vitest ",
|
||||||
|
"test:pipeline": "vitest run"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
@@ -51,9 +54,30 @@
|
|||||||
"@testing-library/react": "^13.1.1",
|
"@testing-library/react": "^13.1.1",
|
||||||
"@testing-library/react-hooks": "^8.0.0",
|
"@testing-library/react-hooks": "^8.0.0",
|
||||||
"@testing-library/user-event": "^14.1.1",
|
"@testing-library/user-event": "^14.1.1",
|
||||||
|
"@types/node": "^18.7.16",
|
||||||
|
"@types/react": "^18.0.19",
|
||||||
|
"@types/react-dom": "^18.0.6",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.37.0",
|
||||||
|
"@typescript-eslint/parser": "^5.37.0",
|
||||||
|
"@vitejs/plugin-react": "^2.1.0",
|
||||||
|
"eslint": "^8.23.1",
|
||||||
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
"eslint-plugin-jest": "^27.0.4",
|
||||||
|
"eslint-plugin-react": "^7.31.8",
|
||||||
|
"eslint-plugin-simple-import-sort": "^7.0.0",
|
||||||
|
"eslint-plugin-testing-library": "^5.6.4",
|
||||||
|
"jsdom": "^20.0.0",
|
||||||
|
"prettier": "^2.7.1",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react-scripts": "^5.0.1",
|
|
||||||
"react-test-renderer": "^18.1.0",
|
"react-test-renderer": "^18.1.0",
|
||||||
"sass": "^1.51.0"
|
"sass": "^1.51.0",
|
||||||
|
"stylelint": "^14.9.1",
|
||||||
|
"stylelint-config-prettier": "^9.0.3",
|
||||||
|
"stylelint-config-standard-scss": "^4.0.0",
|
||||||
|
"typescript": "^4.8.3",
|
||||||
|
"vite": "^3.1.1",
|
||||||
|
"vite-plugin-svgr": "^2.2.1",
|
||||||
|
"vite-tsconfig-paths": "^3.5.0",
|
||||||
|
"vitest": "^0.23.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<meta name="theme-color" content="#121212" />
|
|
||||||
<meta name="onsite" content="onsite - event timer manager" />
|
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
|
||||||
<link
|
|
||||||
rel="icon"
|
|
||||||
type="image/png"
|
|
||||||
sizes="32x32"
|
|
||||||
href="%PUBLIC_URL%/favicon-32x32.png"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="icon"
|
|
||||||
type="image/png"
|
|
||||||
sizes="16x16"
|
|
||||||
href="%PUBLIC_URL%/favicon-16x16.png"
|
|
||||||
/>
|
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/site.webmanifest" />
|
|
||||||
<!--
|
|
||||||
manifest.json provides metadata used when your web app is installed on a
|
|
||||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
|
||||||
-->
|
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
|
||||||
<!--
|
|
||||||
Notice the use of %PUBLIC_URL% in the tags above.
|
|
||||||
It will be replaced with the URL of the `public` folder during the build.
|
|
||||||
Only files inside the `public` folder can be referenced from the HTML.
|
|
||||||
|
|
||||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
|
||||||
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>ontime</title>
|
|
||||||
<style>
|
|
||||||
body,
|
|
||||||
html {
|
|
||||||
background-color: rgba(0, 0, 0, 0) !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
||||||
<div id="root"></div>
|
|
||||||
<!--
|
|
||||||
This HTML file is a template.
|
|
||||||
If you open it directly in the browser, you will see an empty page.
|
|
||||||
|
|
||||||
You can add webfonts, meta tags, or analytics to this file.
|
|
||||||
The build step will place the bundled scripts into the <body> tag.
|
|
||||||
|
|
||||||
To begin the development, run `npm start` or `yarn start`.
|
|
||||||
To create a production bundle, use `npm run build` or `yarn build`.
|
|
||||||
-->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,42 +1,20 @@
|
|||||||
import React, { lazy, Suspense, useCallback, useEffect } from 'react';
|
import { Suspense, useCallback, useEffect } from 'react';
|
||||||
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import './App.scss';
|
import { ChakraProvider } from '@chakra-ui/react';
|
||||||
import withSocket from 'features/viewers/ViewWrapper';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||||
import ErrorBoundary from 'common/components/errorBoundary/ErrorBoundary';
|
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/ProtectedEditor'));
|
import { AppContextProvider } from './common/context/AppContext';
|
||||||
const Table = lazy(() => import('features/table/ProtectedTable'));
|
import SocketProvider from './common/context/socketContext';
|
||||||
|
import theme from './theme/theme';
|
||||||
|
import AppRouter from './AppRouter';
|
||||||
|
|
||||||
const TimerView = lazy(() => import('features/viewers/timer/Timer'));
|
// Load Open Sans typeface
|
||||||
const MinimalTimerView = lazy(() => import('features/viewers/timer/MinimalTimer'));
|
import('typeface-open-sans');
|
||||||
|
export const ontimeQueryClient = new QueryClient();
|
||||||
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 Pip = lazy(() => import('features/viewers/production/Pip'));
|
|
||||||
const StudioClock = lazy(() => import('features/viewers/studio/StudioClock'));
|
|
||||||
|
|
||||||
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() {
|
function App() {
|
||||||
const { data } = useFetch(ALIASES, getAliases);
|
|
||||||
const location = useLocation();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
// Handle keyboard shortcuts
|
// Handle keyboard shortcuts
|
||||||
const handleKeyPress = useCallback((e) => {
|
const handleKeyPress = useCallback((e) => {
|
||||||
@@ -64,86 +42,25 @@ function App() {
|
|||||||
};
|
};
|
||||||
}, [handleKeyPress]);
|
}, [handleKeyPress]);
|
||||||
|
|
||||||
// navigate if is alias route
|
|
||||||
useEffect(() => {
|
|
||||||
if (data == null) return;
|
|
||||||
for (const d of data) {
|
|
||||||
if (`/${d.alias}` === location.pathname && d.enabled) {
|
|
||||||
navigate(`/${d.pathAndParams}`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [data, location, navigate]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='App'>
|
<ChakraProvider resetCSS theme={theme}>
|
||||||
<ErrorBoundary>
|
<SocketProvider>
|
||||||
<Suspense fallback={null}>
|
<QueryClientProvider client={ontimeQueryClient}>
|
||||||
<Routes>
|
<AppContextProvider>
|
||||||
<Route path='/' element={<STimer />} />
|
<BrowserRouter>
|
||||||
<Route path='/speaker' element={<STimer />} />
|
<div className='App'>
|
||||||
<Route path='/presenter' element={<STimer />} />
|
<ErrorBoundary>
|
||||||
<Route path='/stage' element={<STimer />} />
|
<Suspense fallback={null}>
|
||||||
<Route path='/timer' element={<STimer />} />
|
<AppRouter />
|
||||||
|
</Suspense>
|
||||||
<Route path='/minimal' element={<SMinimalTimer />} />
|
</ErrorBoundary>
|
||||||
<Route path='/minimalTimer' element={<SMinimalTimer />} />
|
<ReactQueryDevtools initialIsOpen={false} />
|
||||||
<Route path='/simpleTimer' element={<SMinimalTimer />} />
|
</div>
|
||||||
|
</BrowserRouter>
|
||||||
<Route path='/sm' element={<SStageManager />} />
|
</AppContextProvider>
|
||||||
<Route path='/backstage' element={<SStageManager />} />
|
</QueryClientProvider>
|
||||||
|
</SocketProvider>
|
||||||
<Route path='/public' element={<SPublic />} />
|
</ChakraProvider>
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
* {
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
scrollbar-color: rgba(255, 255, 255, 0.15) rgba(255, 255, 255, 0.01);
|
|
||||||
scrollbar-width: thin;
|
|
||||||
}
|
|
||||||
|
|
||||||
body,
|
|
||||||
html,
|
|
||||||
.App {
|
|
||||||
margin: 0 auto;
|
|
||||||
overflow: hidden;
|
|
||||||
overflow: clip;
|
|
||||||
height: 100vh;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-app-region: drag;
|
|
||||||
}
|
|
||||||
|
|
||||||
option {
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* width */
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 0.5rem;
|
|
||||||
height: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Track */
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: rgba(255, 255, 255, 0.01);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Handle */
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: rgba(255, 255, 255, 0.15);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Handle on hover */
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.35);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { lazy, useEffect } from 'react';
|
||||||
|
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { ALIASES } from './common/api/apiConstants';
|
||||||
|
import { getAliases } from './common/api/ontimeApi';
|
||||||
|
import { useFetch } from './common/hooks/useFetch';
|
||||||
|
import { useSocketProvider } from './common/hooks/useSocketProvider';
|
||||||
|
import withSocket from './features/viewers/ViewWrapper';
|
||||||
|
|
||||||
|
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/minimal-timer/MinimalTimer'));
|
||||||
|
const ClockView = lazy(() => import('features/viewers/clock/Clock'));
|
||||||
|
const Countdown = lazy(() => import('features/viewers/countdown/Countdown'));
|
||||||
|
|
||||||
|
const Backstage = lazy(() => import('features/viewers/backstage/Backstage'));
|
||||||
|
const Public = lazy(() => import('features/viewers/public/Public'));
|
||||||
|
const Lower = lazy(() => import('features/viewers/lower-thirds/LowerWrapper'));
|
||||||
|
const Pip = lazy(() => import('features/viewers/picture-in-picture/Pip'));
|
||||||
|
const StudioClock = lazy(() => import('features/viewers/studio/StudioClock'));
|
||||||
|
|
||||||
|
const STimer = withSocket(TimerView);
|
||||||
|
const SMinimalTimer = withSocket(MinimalTimerView);
|
||||||
|
const SClock = withSocket(ClockView);
|
||||||
|
const SCountdown = withSocket(Countdown);
|
||||||
|
const SBackstage = withSocket(Backstage);
|
||||||
|
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'));
|
||||||
|
|
||||||
|
export default function AppRouter() {
|
||||||
|
useSocketProvider();
|
||||||
|
const { data } = useFetch(ALIASES, getAliases);
|
||||||
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// navigate if is alias route
|
||||||
|
useEffect(() => {
|
||||||
|
if (!data) return;
|
||||||
|
for (const d of data) {
|
||||||
|
if (`/${d.alias}` === location.pathname && d.enabled) {
|
||||||
|
navigate(`/${d.pathAndParams}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [data, location, navigate]);
|
||||||
|
|
||||||
|
return(
|
||||||
|
<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='/clock' element={<SClock />} />
|
||||||
|
|
||||||
|
<Route path='/countdown' element={<SCountdown />} />
|
||||||
|
|
||||||
|
<Route path='/sm' element={<SBackstage />} />
|
||||||
|
<Route path='/backstage' element={<SBackstage />} />
|
||||||
|
|
||||||
|
<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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import {QueryClient} from "react-query";
|
import { QueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
export const queryClientMock = new QueryClient({
|
export const queryClientMock = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
@@ -6,4 +6,4 @@ export const queryClientMock = new QueryClient({
|
|||||||
retry: false,
|
retry: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
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 = () => {
|
|
||||||
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`;
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
|
||||||
import io from 'socket.io-client';
|
|
||||||
import { serverURL } from 'app/api/apiConstants';
|
|
||||||
|
|
||||||
const SocketContext = createContext([[], () => {}]);
|
|
||||||
|
|
||||||
export const useSocket = () => {
|
|
||||||
return useContext(SocketContext);
|
|
||||||
};
|
|
||||||
|
|
||||||
function SocketProvider({ children }) {
|
|
||||||
const [socket, setSocket] = useState();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const s = io(serverURL, { transports: ['websocket'] });
|
|
||||||
setSocket(s);
|
|
||||||
return () => s.disconnect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return <SocketContext.Provider value={socket}>{children}</SocketContext.Provider>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SocketProvider;
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
const dummy = new Date();
|
|
||||||
|
|
||||||
export const sampleData = {
|
|
||||||
presenterMessage: {
|
|
||||||
text: 'Only the timer sees this',
|
|
||||||
active: false,
|
|
||||||
},
|
|
||||||
publicMessage: {
|
|
||||||
text: 'Everyone sees this',
|
|
||||||
active: false,
|
|
||||||
},
|
|
||||||
events: [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
title: 'Is the internet a fad?',
|
|
||||||
subtitle: 'It is',
|
|
||||||
presenter: 'Carlos Valente',
|
|
||||||
timeStart: 37800000,
|
|
||||||
timeEnd: 39600000,
|
|
||||||
clockStarted: dummy,
|
|
||||||
timerDuration: 60,
|
|
||||||
type: 'event',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 0.4849093424577693,
|
|
||||||
duration: 25*60000,
|
|
||||||
type: 'delay',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
title: 'Is reddit a dictatorship?',
|
|
||||||
subtitle: 'It is',
|
|
||||||
presenter: 'Carlos Valente',
|
|
||||||
timeStart: 39600000,
|
|
||||||
timeEnd: 40320000,
|
|
||||||
clockStarted: dummy,
|
|
||||||
timerDuration: 60*60000,
|
|
||||||
type: 'event',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
title: 'Out of words',
|
|
||||||
subtitle: '',
|
|
||||||
presenter: 'Carlos Valente',
|
|
||||||
timeStart: 41400000,
|
|
||||||
timeEnd: 43200000,
|
|
||||||
clockStarted: dummy,
|
|
||||||
timerDuration: 60,
|
|
||||||
type: 'event',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
// Exported viewer link location
|
// Exported viewer link location
|
||||||
|
const minimalLocation = 'minimal';
|
||||||
const speakerLocation = 'speaker';
|
const speakerLocation = 'speaker';
|
||||||
const smLocation = 'sm';
|
const smLocation = 'sm';
|
||||||
const publicLocation = 'public';
|
const publicLocation = 'public';
|
||||||
@@ -7,6 +8,7 @@ const studioLocation = 'studio';
|
|||||||
const cuesheetLocation = 'cuesheet';
|
const cuesheetLocation = 'cuesheet';
|
||||||
|
|
||||||
export const viewerLocations = [
|
export const viewerLocations = [
|
||||||
|
{ link: minimalLocation, label: 'Minimal timer' },
|
||||||
{ link: speakerLocation, label: 'Speaker Screen' },
|
{ link: speakerLocation, label: 'Speaker Screen' },
|
||||||
{ link: smLocation, label: 'Backstage Screen' },
|
{ link: smLocation, label: 'Backstage Screen' },
|
||||||
{ link: publicLocation, label: 'Public Screen' },
|
{ link: publicLocation, label: 'Public Screen' },
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
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'];
|
||||||
|
export const VIEW_SETTINGS = ['viewSettings'];
|
||||||
|
|
||||||
|
export const FEAT_EVENTLIST = ['feat-eventList'];
|
||||||
|
export const FEAT_MESSAGECONTROL = ['feat-messagecontrol'];
|
||||||
|
export const FEAT_PLAYBACKCONTROL = ['feat-playbackcontrol'];
|
||||||
|
export const FEAT_INFO = ['feat-info'];
|
||||||
|
export const FEAT_CUESHEET = ['feat-cuesheet'];
|
||||||
|
export const TIMER = ['ontime-timer'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description finds server path given the current location, it
|
||||||
|
* @return {*}
|
||||||
|
*/
|
||||||
|
export const calculateServer = () =>
|
||||||
|
import.meta.env.DEV ? `http://localhost:${STATIC_PORT}` : 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 stylesPath = 'external/styles/override.css';
|
||||||
|
export const overrideStylesURL = `serverURL/${stylesPath}`;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
import { eventURL } from './apiConstants';
|
import { eventURL } from './apiConstants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
import { eventsURL } from './apiConstants';
|
import { eventsURL } from './apiConstants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
import { ontimeURL } from './apiConstants';
|
import { ontimeURL } from './apiConstants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -18,7 +19,12 @@ export const ontimePlaceholderInfo = {
|
|||||||
* @type {{pinCode: null}}
|
* @type {{pinCode: null}}
|
||||||
*/
|
*/
|
||||||
export const ontimePlaceholderSettings = {
|
export const ontimePlaceholderSettings = {
|
||||||
|
app: 'ontime',
|
||||||
|
version: 1,
|
||||||
|
serverPort: 4001,
|
||||||
|
lock: null,
|
||||||
pinCode: null,
|
pinCode: null,
|
||||||
|
timeFormat: '24',
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,6 +67,14 @@ export const oscPlaceholderSettings = {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description placeholder information for view settings
|
||||||
|
* @type {{overrideCSS: boolean}}
|
||||||
|
*/
|
||||||
|
export const viewsPlaceholder = {
|
||||||
|
overrideStyles: false,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description placeholder information for httpSettings
|
* @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}}}
|
* @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}}}
|
||||||
@@ -157,6 +171,21 @@ export const getInfo = async () => {
|
|||||||
*/
|
*/
|
||||||
export const postInfo = async (data) => axios.post(`${ontimeURL}/info`, data);
|
export const postInfo = async (data) => axios.post(`${ontimeURL}/info`, data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description HTTP request to retrieve view settings
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
export const getView = async () => {
|
||||||
|
const res = await axios.get(`${ontimeURL}/views`);
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description HTTP request to mutate view settings
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
export const postView = async (data) => axios.post(`${ontimeURL}/views`, data);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description HTTP request to retrieve aliases
|
* @description HTTP request to retrieve aliases
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
@@ -222,9 +251,7 @@ export const downloadEvents = async () => {
|
|||||||
filename = headerLine.substring(startFileNameIndex, endFileNameIndex);
|
filename = headerLine.substring(startFileNameIndex, endFileNameIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = window.URL.createObjectURL(
|
const url = window.URL.createObjectURL(new Blob([response.data], { type: 'application/json' }));
|
||||||
new Blob([response.data], { type: 'application/json' })
|
|
||||||
);
|
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = url;
|
link.href = url;
|
||||||
link.setAttribute('download', filename);
|
link.setAttribute('download', filename);
|
||||||
@@ -237,18 +264,26 @@ export const downloadEvents = async () => {
|
|||||||
* @description HTTP request to upload events db
|
* @description HTTP request to upload events db
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
export const uploadEvents = async (file) => {
|
export const uploadEvents = async (file, setProgress, options) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('userFile', file); // appending file
|
formData.append('userFile', file);
|
||||||
await axios.post(`${ontimeURL}/db`, formData, {
|
const onlyEvents = options?.onlyEvents;
|
||||||
headers: {
|
await axios
|
||||||
'Content-Type': 'multipart/form-data',
|
.post(`${ontimeURL}/db?onlyEvents=${onlyEvents}`, formData, {
|
||||||
},
|
headers: {
|
||||||
});
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
onUploadProgress: (progressEvent) => {
|
||||||
|
const complete = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
||||||
|
setProgress(complete);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((response) => response.data.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description HTTP request to upload events
|
* @description HTTP request to upload events
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
export const uploadEventsWithPath = async (filepath) => axios.post(`${ontimeURL}/dbpath`, { path: filepath });
|
export const uploadEventsWithPath = async (filepath) =>
|
||||||
|
axios.post(`${ontimeURL}/dbpath`, { path: filepath });
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
import { playbackURL } from './apiConstants';
|
import { playbackURL } from './apiConstants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { atomWithStorage, selectAtom } from 'jotai/utils';
|
||||||
|
|
||||||
|
export const eventSettingsAtom = atomWithStorage('ontime-eventSettings', {
|
||||||
|
showQuickEntry: false,
|
||||||
|
startTimeIsLastEnd: false,
|
||||||
|
defaultPublic: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const showQuickEntryAtom = selectAtom(
|
||||||
|
eventSettingsAtom,
|
||||||
|
(settings) => settings.showQuickEntry
|
||||||
|
);
|
||||||
|
export const startTimeIsLastEndAtom = selectAtom(
|
||||||
|
eventSettingsAtom,
|
||||||
|
(settings) => settings.startTimeIsLastEnd
|
||||||
|
);
|
||||||
|
export const defaultPublicAtom = selectAtom(
|
||||||
|
eventSettingsAtom,
|
||||||
|
(settings) => settings.defaultPublic
|
||||||
|
);
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/menu';
|
|
||||||
import { IconButton } from '@chakra-ui/button';
|
import { IconButton } from '@chakra-ui/button';
|
||||||
|
import { Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/menu';
|
||||||
|
import { Tooltip } from '@chakra-ui/tooltip';
|
||||||
import { FiClock } from '@react-icons/all-files/fi/FiClock';
|
import { FiClock } from '@react-icons/all-files/fi/FiClock';
|
||||||
import { FiMinusCircle } from '@react-icons/all-files/fi/FiMinusCircle';
|
import { FiMinusCircle } from '@react-icons/all-files/fi/FiMinusCircle';
|
||||||
import { FiPlus } from '@react-icons/all-files/fi/FiPlus';
|
import { FiPlus } from '@react-icons/all-files/fi/FiPlus';
|
||||||
import { Tooltip } from '@chakra-ui/tooltip';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||||
|
|
||||||
export default function ActionButtons(props) {
|
export default function ActionButtons(props) {
|
||||||
const { showAdd, showDelay, showBlock, actionHandler } = props;
|
const { showAdd, showDelay, showBlock, actionHandler } = props;
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ export default function ActionButtons(props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu isLazy lazyBehavior='unmount'>
|
<Menu isLazy lazyBehavior='unmount'>
|
||||||
<Tooltip label='Add ...' delay={500}>
|
<Tooltip label='Add ...' delay={tooltipDelayMid}>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
as={IconButton}
|
as={IconButton}
|
||||||
aria-label='Options'
|
aria-label='Options'
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
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 { Button } from '@chakra-ui/button';
|
||||||
|
import { IoCheckmarkSharp } from '@react-icons/all-files/io5/IoCheckmarkSharp';
|
||||||
|
import { IoCloseSharp } from '@react-icons/all-files/io5/IoCloseSharp';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
export default function EnableBtn(props) {
|
export default function EnableBtn(props) {
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import React from 'react';
|
|
||||||
import { IconButton } from '@chakra-ui/button';
|
import { IconButton } from '@chakra-ui/button';
|
||||||
import { IoPause } from '@react-icons/all-files/io5/IoPause';
|
|
||||||
import { Tooltip } from '@chakra-ui/tooltip';
|
import { Tooltip } from '@chakra-ui/tooltip';
|
||||||
|
import { IoPause } from '@react-icons/all-files/io5/IoPause';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||||
|
|
||||||
export default function PauseIconBtn(props) {
|
export default function PauseIconBtn(props) {
|
||||||
const { clickhandler, active, disabled, ...rest } = props;
|
const { clickhandler, active, disabled, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<Tooltip label='Pause timer' openDelay={500} shouldWrapChildren={disabled}>
|
<Tooltip label='Pause timer' openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<IoPause size='24px' />}
|
icon={<IoPause size='24px' />}
|
||||||
colorScheme='orange'
|
colorScheme='orange'
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from 'react';
|
|
||||||
import { IconButton } from '@chakra-ui/button';
|
import { IconButton } from '@chakra-ui/button';
|
||||||
import { Tooltip } from '@chakra-ui/tooltip';
|
import { Tooltip } from '@chakra-ui/tooltip';
|
||||||
import { FiUsers } from '@react-icons/all-files/fi/FiUsers';
|
import { FiUsers } from '@react-icons/all-files/fi/FiUsers';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||||
import { Button, IconButton } from '@chakra-ui/button';
|
import { Button, IconButton } from '@chakra-ui/button';
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
@@ -11,7 +11,8 @@ import {
|
|||||||
import { Tooltip } from '@chakra-ui/tooltip';
|
import { Tooltip } from '@chakra-ui/tooltip';
|
||||||
import { FiPower } from '@react-icons/all-files/fi/FiPower';
|
import { FiPower } from '@react-icons/all-files/fi/FiPower';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { LoggingContext } from '../../../app/context/LoggingContext';
|
|
||||||
|
import { LoggingContext } from '../../context/LoggingContext';
|
||||||
|
|
||||||
export default function QuitIconBtn(props) {
|
export default function QuitIconBtn(props) {
|
||||||
const { clickHandler, size = 'lg', ...rest } = props;
|
const { clickHandler, size = 'lg', ...rest } = props;
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import React from 'react';
|
|
||||||
import { IconButton } from '@chakra-ui/button';
|
import { IconButton } from '@chakra-ui/button';
|
||||||
import { IoTimeOutline } from '@react-icons/all-files/io5/IoTimeOutline';
|
|
||||||
import { Tooltip } from '@chakra-ui/tooltip';
|
import { Tooltip } from '@chakra-ui/tooltip';
|
||||||
|
import { IoTimeOutline } from '@react-icons/all-files/io5/IoTimeOutline';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||||
|
|
||||||
export default function RollIconBtn(props) {
|
export default function RollIconBtn(props) {
|
||||||
const { clickhandler, active, disabled, ...rest } = props;
|
const { clickhandler, active, disabled, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<Tooltip label='Roll mode' openDelay={500} shouldWrapChildren={disabled}>
|
<Tooltip label='Roll mode' openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<IoTimeOutline size='24px' />}
|
icon={<IoTimeOutline size='24px' />}
|
||||||
colorScheme='blue'
|
colorScheme='blue'
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import React from 'react';
|
|
||||||
import { IconButton } from '@chakra-ui/button';
|
import { IconButton } from '@chakra-ui/button';
|
||||||
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
|
|
||||||
import { Tooltip } from '@chakra-ui/tooltip';
|
import { Tooltip } from '@chakra-ui/tooltip';
|
||||||
|
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||||
|
|
||||||
export default function StartIconBtn(props) {
|
export default function StartIconBtn(props) {
|
||||||
const { clickhandler, active, disabled, ...rest } = props;
|
const { clickhandler, active, disabled, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<Tooltip label='Start timer' openDelay={500} shouldWrapChildren={disabled}>
|
<Tooltip label='Start timer' openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<IoPlay size='24px' />}
|
icon={<IoPlay size='24px' />}
|
||||||
colorScheme='green'
|
colorScheme='green'
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
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,25 @@
|
|||||||
|
import { IconButton } from '@chakra-ui/button';
|
||||||
|
import { IconButtonProps } from '@chakra-ui/react';
|
||||||
|
import { Tooltip } from '@chakra-ui/tooltip';
|
||||||
|
|
||||||
|
interface TooltipActionBtnProps extends IconButtonProps {
|
||||||
|
clickHandler: () => void;
|
||||||
|
tooltip: string;
|
||||||
|
openDelay?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TooltipActionBtn(props: TooltipActionBtnProps) {
|
||||||
|
const { clickHandler, icon, size = 'xs', tooltip, openDelay = 0, className, ...rest } = props;
|
||||||
|
return (
|
||||||
|
<Tooltip label={tooltip} openDelay={openDelay}>
|
||||||
|
<IconButton
|
||||||
|
{...rest}
|
||||||
|
aria-label={tooltip}
|
||||||
|
size={size}
|
||||||
|
icon={icon}
|
||||||
|
onClick={clickHandler}
|
||||||
|
className={className}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { IconButton } from '@chakra-ui/button';
|
import { IconButton } from '@chakra-ui/button';
|
||||||
import { Tooltip } from '@chakra-ui/tooltip';
|
import { Tooltip } from '@chakra-ui/tooltip';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import React from 'react';
|
|
||||||
import { IconButton } from '@chakra-ui/button';
|
import { IconButton } from '@chakra-ui/button';
|
||||||
import { Tooltip } from '@chakra-ui/tooltip';
|
import { Tooltip } from '@chakra-ui/tooltip';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||||
|
|
||||||
export default function TransportIconBtn(props) {
|
export default function TransportIconBtn(props) {
|
||||||
const { clickHandler, icon, tooltip, disabled, ...rest } = props;
|
const { clickHandler, icon, tooltip, disabled, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<Tooltip label={tooltip} openDelay={500} shouldWrapChildren={disabled}>
|
<Tooltip label={tooltip} openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={icon}
|
icon={icon}
|
||||||
colorScheme='white'
|
colorScheme='white'
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import React from 'react';
|
|
||||||
import { IconButton } from '@chakra-ui/button';
|
import { IconButton } from '@chakra-ui/button';
|
||||||
import { IoStop } from '@react-icons/all-files/io5/IoStop';
|
|
||||||
import { Tooltip } from '@chakra-ui/tooltip';
|
import { Tooltip } from '@chakra-ui/tooltip';
|
||||||
|
import { IoStop } from '@react-icons/all-files/io5/IoStop';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||||
|
|
||||||
export default function UnloadIconBtn(props) {
|
export default function UnloadIconBtn(props) {
|
||||||
const { clickHandler, disabled, ...rest } = props;
|
const { clickHandler, disabled, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<Tooltip label='Unload event' openDelay={500} shouldWrapChildren={disabled}>
|
<Tooltip label='Unload event' openDelay={tooltipDelayMid} shouldWrapChildren={disabled}>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<IoStop size='22px' />}
|
icon={<IoStop size='22px' />}
|
||||||
colorScheme='red'
|
colorScheme='red'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Icon } from '@chakra-ui/react';
|
import { Icon } from '@chakra-ui/react';
|
||||||
import { FiChevronUp } from '@react-icons/all-files/fi/FiChevronUp';
|
import { FiChevronUp } from '@react-icons/all-files/fi/FiChevronUp';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import style from './CollapseBar.module.scss';
|
import style from './CollapseBar.module.scss';
|
||||||
|
|
||||||
export default function CollapseBar(props) {
|
export default function CollapseBar(props) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@use '../../../styles/main' as *;
|
@use '../../../theme/main' as *;
|
||||||
|
|
||||||
.header,
|
.header,
|
||||||
.headerRoll {
|
.headerRoll {
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import React, { memo } from 'react';
|
|
||||||
import { formatDisplay } from 'common/utils/dateConfig';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import styles from './Countdown.module.scss';
|
|
||||||
|
|
||||||
const Countdown = ({ time, small, isNegative, hideZeroHours }) => {
|
|
||||||
// prepare display string
|
|
||||||
const display =
|
|
||||||
time != null && !isNaN(time) ? formatDisplay(time, hideZeroHours) : '-- : -- : --';
|
|
||||||
|
|
||||||
const classes = `${small ? styles.countdownClockSmall : styles.countdownClock}
|
|
||||||
${isNegative ? styles.negative : ''}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classes}>
|
|
||||||
{display}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(Countdown);
|
|
||||||
|
|
||||||
Countdown.propTypes = {
|
|
||||||
time: PropTypes.number,
|
|
||||||
small: PropTypes.bool,
|
|
||||||
isNegative: PropTypes.bool,
|
|
||||||
hideZeroHours: PropTypes.bool,
|
|
||||||
};
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
@use '../../../styles/main' as *;
|
|
||||||
|
|
||||||
/* Common */
|
|
||||||
.countdownClock,
|
|
||||||
.countdownClockSmall {
|
|
||||||
font-family: 'Open Sans', sans-serif;
|
|
||||||
line-height: 0.9;
|
|
||||||
color: $text-white;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Viewers */
|
|
||||||
.countdownClock {
|
|
||||||
font-size: 21vw;
|
|
||||||
line-height: 21vw;
|
|
||||||
text-align: center;
|
|
||||||
letter-spacing: 1vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Editor */
|
|
||||||
.countdownClockSmall {
|
|
||||||
font-size: 4em;
|
|
||||||
text-align: center;
|
|
||||||
letter-spacing: 0.1em;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.negative {
|
|
||||||
color: $ontime-pink;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { memo } from 'react';
|
||||||
|
import { formatDisplay } from 'common/utils/dateConfig';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import './TimerDisplay.scss';
|
||||||
|
|
||||||
|
const TimerDisplay = ({ time, small, isNegative, hideZeroHours }) => {
|
||||||
|
|
||||||
|
const display =
|
||||||
|
(time === null || typeof time === 'undefined' || isNaN(time))
|
||||||
|
? '-- : -- : --'
|
||||||
|
: formatDisplay(time, hideZeroHours);
|
||||||
|
|
||||||
|
const classes = `timer ${small ? 'timer--small' : ''} ${isNegative ? 'timer--finished' : ''}`;
|
||||||
|
|
||||||
|
return <div className={classes}>{display}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(TimerDisplay);
|
||||||
|
|
||||||
|
TimerDisplay.propTypes = {
|
||||||
|
time: PropTypes.number,
|
||||||
|
small: PropTypes.bool,
|
||||||
|
isNegative: PropTypes.bool,
|
||||||
|
hideZeroHours: PropTypes.bool,
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
@use '../../../theme/viewerDefs' as *;
|
||||||
|
|
||||||
|
.timer {
|
||||||
|
font-family: 'Open Sans', sans-serif;
|
||||||
|
color: var(--timer-color-override, $viewer-color);
|
||||||
|
font-size: 21vw;
|
||||||
|
line-height: 21vw;
|
||||||
|
text-align: center;
|
||||||
|
letter-spacing: 1vw;
|
||||||
|
|
||||||
|
&--small {
|
||||||
|
font-size: 4em;
|
||||||
|
line-height: 0.9;
|
||||||
|
text-align: center;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--finished {
|
||||||
|
color: $timer-finished-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
/* eslint-disable react/destructuring-assignment */
|
/* eslint-disable react/destructuring-assignment */
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { LoggingContext } from '../../../app/context/LoggingContext';
|
|
||||||
|
import { version as appVersion } from '../../../../package.json';
|
||||||
|
import { LoggingContext } from '../../context/LoggingContext';
|
||||||
|
|
||||||
import style from './ErrorBoundary.module.scss';
|
import style from './ErrorBoundary.module.scss';
|
||||||
const appVersion = require('../../../../package.json').version;
|
|
||||||
|
|
||||||
class ErrorBoundary extends React.Component {
|
class ErrorBoundary extends React.Component {
|
||||||
static contextType = LoggingContext;
|
static contextType = LoggingContext;
|
||||||
@@ -25,8 +27,8 @@ class ErrorBoundary extends React.Component {
|
|||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
this.context.emitError(error.toString());
|
this.context.emitError(error.toString());
|
||||||
} catch {
|
} catch (e) {
|
||||||
console.log('Unable to emit error')
|
console.log('Unable to emit error', error, e )
|
||||||
}
|
}
|
||||||
this.reportContent = `${error} ${info.componentStack}`;
|
this.reportContent = `${error} ${info.componentStack}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@use '../../../styles/main' as *;
|
@use '../../../theme/main' as *;
|
||||||
|
|
||||||
.errorContainer {
|
.errorContainer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import React, { useCallback, useContext } from 'react';
|
import { useCallback, useContext } from 'react';
|
||||||
import EditableTimer from 'common/input/EditableTimer';
|
import EditableTimer from 'common/components/input/EditableTimer';
|
||||||
import { LoggingContext } from '../../../app/context/LoggingContext';
|
|
||||||
import { validateTimes } from '../../../app/entryValidator';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { LoggingContext } from '../../context/LoggingContext';
|
||||||
|
import { validateTimes } from '../../utils/entryValidator';
|
||||||
|
|
||||||
export default function EventTimes(props) {
|
export default function EventTimes(props) {
|
||||||
const { actionHandler, delay, timeStart, timeEnd, previousEnd } = props;
|
const { actionHandler, delay, timeStart, timeEnd, previousEnd } = props;
|
||||||
const { emitWarning } = useContext(LoggingContext);
|
const { emitWarning } = useContext(LoggingContext);
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import React, { useCallback, useContext } from 'react';
|
import { useCallback, useContext } from 'react';
|
||||||
import { LoggingContext } from '../../../app/context/LoggingContext';
|
|
||||||
import { validateTimes } from '../../../app/entryValidator';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import TimesDelayed from './TimesDelayed';
|
|
||||||
|
import { LoggingContext } from '../../context/LoggingContext';
|
||||||
|
import { validateTimes } from '../../utils/entryValidator';
|
||||||
|
|
||||||
import Times from './Times';
|
import Times from './Times';
|
||||||
|
import TimesDelayed from './TimesDelayed';
|
||||||
|
|
||||||
export default function EventTimesVertical(props) {
|
export default function EventTimesVertical(props) {
|
||||||
const { delay, timeStart, timeEnd, duration, previousEnd, actionHandler } = props;
|
const { delay, timeStart, timeEnd, duration, previousEnd, actionHandler } = props;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React from 'react';
|
|
||||||
import EditableTimer from '../../input/EditableTimer';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import style from './Times.module.scss'
|
|
||||||
|
import EditableTimer from '../input/EditableTimer';
|
||||||
|
|
||||||
|
import style from './Times.module.scss';
|
||||||
|
|
||||||
export default function Times(props) {
|
export default function Times(props) {
|
||||||
const { handleValidate, actionHandler, timeStart, timeEnd, duration, previousEnd } = props;
|
const { handleValidate, actionHandler, timeStart, timeEnd, duration, previousEnd } = props;
|
||||||
@@ -37,7 +38,7 @@ export default function Times(props) {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
Times.propTypes = {
|
Times.propTypes = {
|
||||||
handleValidate: PropTypes.func.isRequired,
|
handleValidate: PropTypes.func.isRequired,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@use '../../../styles/main' as *;
|
@use '../../../theme/main' as *;
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
font-size: 0.75em;
|
font-size: 0.75em;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import React from 'react';
|
|
||||||
import EditableTimer from '../../input/EditableTimer';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { stringFromMillis } from '../../utils/time';
|
import { stringFromMillis } from '../../utils/time';
|
||||||
import style from './Times.module.scss'
|
import EditableTimer from '../input/EditableTimer';
|
||||||
|
|
||||||
|
import style from './Times.module.scss';
|
||||||
|
|
||||||
export default function TimesDelayed(props) {
|
export default function TimesDelayed(props) {
|
||||||
const { handleValidate, actionHandler, delay, timeStart, timeEnd, duration, previousEnd } = props;
|
const { handleValidate, actionHandler, delay, timeStart, timeEnd, duration, previousEnd } = props;
|
||||||
@@ -45,7 +46,7 @@ export default function TimesDelayed(props) {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
TimesDelayed.propTypes = {
|
TimesDelayed.propTypes = {
|
||||||
handleValidate: PropTypes.func.isRequired,
|
handleValidate: PropTypes.func.isRequired,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { Textarea } from '@chakra-ui/react';
|
import { Textarea } from '@chakra-ui/react';
|
||||||
import autosize from 'autosize/dist/autosize';
|
import autosize from 'autosize/dist/autosize';
|
||||||
|
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/react';
|
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/react';
|
||||||
import { clamp } from '../../app/utils/math';
|
|
||||||
import style from './TimeInput.module.css';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { clamp } from '../../utils/math';
|
||||||
|
|
||||||
|
import style from './TimeInput.module.css';
|
||||||
|
|
||||||
const inputProps = {
|
const inputProps = {
|
||||||
width: 20,
|
width: 20,
|
||||||
fontWeight: 400,
|
fontWeight: 400,
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
|
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
|
||||||
import style from './EditableText.module.scss';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import style from './EditableText.module.scss';
|
||||||
|
|
||||||
export default function EditableText(props) {
|
export default function EditableText(props) {
|
||||||
const { label, defaultValue, placeholder, submitHandler, maxchar = 40, ...rest } = props;
|
const { label, defaultValue, placeholder, submitHandler, maxchar = 40, ...rest } = props;
|
||||||
const [text, setText] = useState(defaultValue || '');
|
const [text, setText] = useState(defaultValue || '');
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@use '../../styles/main' as *;
|
@use '../../../theme/main' as *;
|
||||||
|
|
||||||
.block {
|
.block {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
import { useCallback, useContext, useEffect, useState } from 'react';
|
||||||
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
|
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
|
||||||
import { forgivingStringToMillis } from '../utils/dateConfig';
|
|
||||||
import { LoggingContext } from '../../app/context/LoggingContext';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { stringFromMillis } from '../utils/time';
|
|
||||||
|
import { LoggingContext } from '../../context/LoggingContext';
|
||||||
|
import { forgivingStringToMillis } from '../../utils/dateConfig';
|
||||||
|
import { stringFromMillis } from '../../utils/time';
|
||||||
|
|
||||||
import style from './EditableTimer.module.scss';
|
import style from './EditableTimer.module.scss';
|
||||||
|
|
||||||
export default function EditableTimer(props) {
|
export default function EditableTimer(props) {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@use '../../styles/main'as *;
|
@use '../../../theme/main'as *;
|
||||||
|
|
||||||
.editable,
|
.editable,
|
||||||
.delayedEditable {
|
.delayedEditable {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import { clamp } from 'common/utils/math';
|
||||||
import { clamp } from 'app/utils/math';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import styles from './MyProgressBar.module.scss';
|
import styles from './MyProgressBar.module.scss';
|
||||||
|
|
||||||
export default function MyProgressBar(props) {
|
export default function MyProgressBar(props) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@use '../../../styles/main' as *;
|
@use '../../../theme/main' as *;
|
||||||
|
|
||||||
.progress,
|
.progress,
|
||||||
.progressCountdown {
|
.progressCountdown {
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Image } from '@chakra-ui/react';
|
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
|
||||||
import navlogo from 'assets/images/logos/LOGO-72.png';
|
|
||||||
import style from './NavLogo.module.scss';
|
|
||||||
import { IconButton } from '@chakra-ui/button';
|
import { IconButton } from '@chakra-ui/button';
|
||||||
|
import { Image } from '@chakra-ui/react';
|
||||||
|
import { IoContract } from '@react-icons/all-files/io5/IoContract';
|
||||||
import { IoExpand } from '@react-icons/all-files/io5/IoExpand';
|
import { IoExpand } from '@react-icons/all-files/io5/IoExpand';
|
||||||
|
import navlogo from 'assets/images/logos/LOGO-72.png';
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import useFullscreen from '../../hooks/useFullscreen';
|
||||||
|
|
||||||
|
import navigatorConstants from './navigatorConstants';
|
||||||
|
|
||||||
|
import style from './NavLogo.module.scss';
|
||||||
|
|
||||||
/* Styling for action buttons */
|
/* Styling for action buttons */
|
||||||
const navButtonStyle = {
|
const navButtonStyle = {
|
||||||
@@ -19,6 +25,7 @@ const navButtonStyle = {
|
|||||||
export default function NavLogo(props) {
|
export default function NavLogo(props) {
|
||||||
const { isHidden } = props;
|
const { isHidden } = props;
|
||||||
const [showNav, setShowNav] = useState(false);
|
const [showNav, setShowNav] = useState(false);
|
||||||
|
const { isFullScreen, toggleFullScreen } = useFullscreen();
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
setShowNav((prev) => !prev);
|
setShowNav((prev) => !prev);
|
||||||
@@ -34,16 +41,6 @@ export default function NavLogo(props) {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const toggleFullscreen = useCallback(() => {
|
|
||||||
if (!document.fullscreenElement) {
|
|
||||||
document.documentElement.requestFullscreen();
|
|
||||||
} else {
|
|
||||||
if (document.exitFullscreen) {
|
|
||||||
document.exitFullscreen();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// attach the event listener
|
// attach the event listener
|
||||||
document.addEventListener('keydown', handleKeyPress);
|
document.addEventListener('keydown', handleKeyPress);
|
||||||
@@ -65,6 +62,7 @@ export default function NavLogo(props) {
|
|||||||
initial={{ opacity: showNav ? 0.5 : baseOpacity }}
|
initial={{ opacity: showNav ? 0.5 : baseOpacity }}
|
||||||
whileHover={{ opacity: 1 }}
|
whileHover={{ opacity: 1 }}
|
||||||
className={style.navContainer}
|
className={style.navContainer}
|
||||||
|
data-testid='nav-logo'
|
||||||
>
|
>
|
||||||
<Image alt='' src={navlogo} className={style.logo} onClick={handleClick} />
|
<Image alt='' src={navlogo} className={style.logo} onClick={handleClick} />
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
@@ -79,8 +77,8 @@ export default function NavLogo(props) {
|
|||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label='Toggle Fullscreen'
|
aria-label='Toggle Fullscreen'
|
||||||
icon={<IoExpand />}
|
icon={isFullScreen ? <IoContract /> : <IoExpand />}
|
||||||
onClick={toggleFullscreen}
|
onClick={toggleFullScreen}
|
||||||
{...navButtonStyle}
|
{...navButtonStyle}
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -91,27 +89,11 @@ export default function NavLogo(props) {
|
|||||||
exit={{ opacity: 0, scaleY: 0, y: -50 }}
|
exit={{ opacity: 0, scaleY: 0, y: -50 }}
|
||||||
className={style.nav}
|
className={style.nav}
|
||||||
>
|
>
|
||||||
<Link to='/timer' {...tabProps}>
|
{navigatorConstants.map((route) => (
|
||||||
Timer
|
<Link to={route.url} key={route.url} {...tabProps}>
|
||||||
</Link>
|
{route.label}
|
||||||
<Link to='/minimal' {...tabProps}>
|
</Link>
|
||||||
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>
|
</motion.div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@use '../../../styles/main' as *;
|
@use '../../../theme/main' as *;
|
||||||
|
|
||||||
.navContainer {
|
.navContainer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
const navigatorConstants = [
|
||||||
|
{ url: '/timer', label: 'Timer' },
|
||||||
|
{ url: '/clock', label: 'Clock' },
|
||||||
|
{ url: '/minimal', label: 'Minimal Timer' },
|
||||||
|
{ url: '/backstage', label: 'Backstage' },
|
||||||
|
{ url: '/public', label: 'Public' },
|
||||||
|
{ url: '/lower', label: 'Lower Thirds' },
|
||||||
|
{ url: '/pip', label: 'PiP' },
|
||||||
|
{ url: '/studio', label: 'Studio Clock' },
|
||||||
|
{ url: '/countdown', label: 'Countdown' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default navigatorConstants;
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import TodayItem from './TodayItem';
|
import { useInterval } from 'common/hooks/useInterval';
|
||||||
import { useInterval } from 'app/hooks/useInterval';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Empty from '../../state/Empty';
|
|
||||||
import style from './Paginator.module.scss';
|
import Empty from '../state/Empty';
|
||||||
|
|
||||||
|
import TodayItem from './TodayItem';
|
||||||
|
|
||||||
|
import './Paginator.scss';
|
||||||
|
|
||||||
export default function Paginator(props) {
|
export default function Paginator(props) {
|
||||||
const {
|
const {
|
||||||
@@ -67,7 +70,7 @@ export default function Paginator(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.entries}>
|
<div className='paginator entries'>
|
||||||
{page.map((e) => {
|
{page.map((e) => {
|
||||||
if (e.id === selectedId) selectedState = 1;
|
if (e.id === selectedId) selectedState = 1;
|
||||||
else if (selectedState === 1) selectedState = 2;
|
else if (selectedState === 1) selectedState = 2;
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
@use '../../../theme/viewerDefs' as *;
|
||||||
|
|
||||||
|
.paginator.entries {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.entry-times {
|
||||||
|
font-family: 'Open Sans', sans-serif;
|
||||||
|
font-size: 1.4vw;
|
||||||
|
|
||||||
|
min-width: 10vw;
|
||||||
|
opacity: 0.9;
|
||||||
|
letter-spacing: 0.035em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-title {
|
||||||
|
align-self: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry {
|
||||||
|
display: flex;
|
||||||
|
font-size: 1.3vw;
|
||||||
|
padding: 0.2vh 1vw;
|
||||||
|
margin-bottom: 1.5vh;
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
&--now,
|
||||||
|
&--future {
|
||||||
|
color: var(--color-override, $viewer-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--past {
|
||||||
|
font-size: 1.2vw;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--now {
|
||||||
|
line-height: 5vh;
|
||||||
|
background-color: $now-bg-color;
|
||||||
|
border-bottom: 0.5vh solid;
|
||||||
|
border-bottom-color: var(--accent-color-override, $accent-color);
|
||||||
|
margin-left: -0.5em;
|
||||||
|
padding-left: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--future {
|
||||||
|
background-color: $future-bg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.backstage-indicator {
|
||||||
|
color: var(--accent-color-override, $accent-color);
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: -0.5vw;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '*';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { formatTime } from '../../utils/time';
|
||||||
|
|
||||||
|
import './Paginator.scss';
|
||||||
|
|
||||||
|
export default function TodayItem(props) {
|
||||||
|
const { selected, timeStart, timeEnd, title, backstageEvent, colour } = props;
|
||||||
|
|
||||||
|
// Format timers
|
||||||
|
const start = formatTime(timeStart, { format: 'hh:mm' });
|
||||||
|
const end = formatTime(timeEnd, { format: 'hh:mm' });
|
||||||
|
|
||||||
|
// user colours
|
||||||
|
const userColour = colour !== '' ? colour : 'transparent';
|
||||||
|
|
||||||
|
// select styling
|
||||||
|
let selectStyle = 'entry--past';
|
||||||
|
if (selected === 1) selectStyle = 'entry--now';
|
||||||
|
else if (selected === 2) selectStyle = 'entry--future';
|
||||||
|
return (
|
||||||
|
<div className={`entry ${selectStyle}`} style={{ borderLeft: `4px solid ${userColour}` }}>
|
||||||
|
<div className='entry-times'>
|
||||||
|
{`${start} · ${end}`}
|
||||||
|
</div>
|
||||||
|
<div className='entry-title'>{title}</div>
|
||||||
|
{backstageEvent && <div className='backstage-indicator' />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TodayItem.propTypes = {
|
||||||
|
selected: PropTypes.number,
|
||||||
|
timeStart: PropTypes.number,
|
||||||
|
timeEnd: PropTypes.number,
|
||||||
|
title: PropTypes.string,
|
||||||
|
backstageEvent: PropTypes.bool,
|
||||||
|
colour: PropTypes.string,
|
||||||
|
};
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
import { 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 { IconButton } from '@chakra-ui/button';
|
||||||
|
import { HStack, PinInput, PinInputField } from '@chakra-ui/react';
|
||||||
import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
|
import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
|
||||||
import { AppContext } from '../../../app/context/AppContext';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { AppContext } from '../../context/AppContext';
|
||||||
|
|
||||||
import style from './ProtectRoute.module.scss';
|
import style from './ProtectRoute.module.scss';
|
||||||
|
|
||||||
export default function ProtectRoute({ children }) {
|
export default function ProtectRoute({ children }) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@use '../../../styles/main' as *;
|
@use '../../../theme/main' as *;
|
||||||
@use '../../../styles/mixins' as *;
|
@use '../../../theme/mixins' as *;
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
background: $bg-black;
|
background: $bg-black;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import React from 'react';
|
|
||||||
import { ReactComponent as Emptyimage } from 'assets/images/empty.svg';
|
import { ReactComponent as Emptyimage } from 'assets/images/empty.svg';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import style from './Empty.module.scss';
|
import style from './Empty.module.scss';
|
||||||
|
|
||||||
export default function Empty(props) {
|
export default function Empty(props) {
|
||||||
const { text, ...rest } = props;
|
const { text, dark, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<div className={style.emptyContainer} {...rest}>
|
<div className={`${style.emptyContainer} ${dark ? style.dark : ''}`} {...rest}>
|
||||||
<Emptyimage className={style.empty} />
|
<Emptyimage className={style.empty} />
|
||||||
<span className={style.text}>{text}</span>
|
<span className={style.text}>{text}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -15,4 +15,5 @@ export default function Empty(props) {
|
|||||||
|
|
||||||
Empty.propTypes = {
|
Empty.propTypes = {
|
||||||
text: PropTypes.string,
|
text: PropTypes.string,
|
||||||
|
dark: PropTypes.bool,
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
@use '../../../theme/main' as *;
|
||||||
|
|
||||||
|
.emptyContainer {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
color: $bg-black-300;
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
width: 100%;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.dark {
|
||||||
|
background: $bg-black;
|
||||||
|
color: $bg-gray-500;
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import style from './TitleCard.module.scss';
|
import style from './TitleCard.module.scss';
|
||||||
|
|
||||||
export default function TitleCard(props) {
|
export default function TitleCard(props) {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@use '../../../styles/main' as *;
|
@use '../../../theme/main' as *;
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
@include card-label;
|
@include card-label;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import './TitleSide.scss';
|
||||||
|
|
||||||
|
export default function TitleSide(props) {
|
||||||
|
const { type, label, title, subtitle, presenter } = props;
|
||||||
|
|
||||||
|
const isNext = type === 'next';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='title-side'>
|
||||||
|
<div className='label'>{label}</div>
|
||||||
|
<div className={isNext ? 'title next' : 'title now'}>{title}</div>
|
||||||
|
<div className={isNext ? 'presenter next' : 'presenter now'}>{presenter}</div>
|
||||||
|
<div className={isNext ? 'subtitle next' : 'subtitle now'}>{subtitle}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TitleSide.propTypes = {
|
||||||
|
type: PropTypes.oneOf(['now', 'next']),
|
||||||
|
label: PropTypes.string,
|
||||||
|
title: PropTypes.string,
|
||||||
|
subtitle: PropTypes.string,
|
||||||
|
presenter: PropTypes.string,
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
@use '../../../theme/viewerDefs' as *;
|
||||||
|
|
||||||
|
.title-side {
|
||||||
|
.label {
|
||||||
|
font-size: 1.3vw;
|
||||||
|
color: var(--accent-color-override, $accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title,
|
||||||
|
.subtitle,
|
||||||
|
.presenter {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: $title-color;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&.now {
|
||||||
|
font-size: 2.5vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.next {
|
||||||
|
font-size: 2vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: $subtitle-color;
|
||||||
|
&.now {
|
||||||
|
font-size: 1.2vw;
|
||||||
|
margin-top: -1vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.next {
|
||||||
|
font-size: 1.1vw;
|
||||||
|
margin-top: -0.8vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.presenter {
|
||||||
|
font-weight: 400;
|
||||||
|
color: $subtitle-color;
|
||||||
|
|
||||||
|
&.now {
|
||||||
|
font-size: 2vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.next {
|
||||||
|
font-size: 1.5vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title:after,
|
||||||
|
.subtitle:after,
|
||||||
|
.presenter:after {
|
||||||
|
content: '\200b';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
@use '../../../theme/main' as *;
|
||||||
|
|
||||||
|
.modalBody {
|
||||||
|
min-height: 40vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.options {
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notes {
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
background-color: $bg-gray;
|
||||||
|
margin: 1em 0;
|
||||||
|
padding: 0.5em;
|
||||||
|
border-radius: 2px;
|
||||||
|
color: black;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.corner {
|
||||||
|
position: absolute;
|
||||||
|
right: 4px;
|
||||||
|
top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoList {
|
||||||
|
font-size: 0.9em;
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flexColumnLeft {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import { ChangeEvent, useCallback, useContext, useRef, useState } from 'react';
|
||||||
|
import { Button } from '@chakra-ui/button';
|
||||||
|
import {
|
||||||
|
Checkbox,
|
||||||
|
FormControl,
|
||||||
|
FormErrorMessage,
|
||||||
|
FormHelperText,
|
||||||
|
FormLabel,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalContent,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
ModalOverlay,
|
||||||
|
Progress,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { IoCloseSharp } from '@react-icons/all-files/io5/IoCloseSharp';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { EVENTS_TABLE } from '../../api/apiConstants';
|
||||||
|
import { uploadEvents } from '../../api/ontimeApi';
|
||||||
|
import { LoggingContext } from '../../context/LoggingContext';
|
||||||
|
import TooltipActionBtn from '../buttons/TooltipActionBtn';
|
||||||
|
|
||||||
|
import { validateFile } from './utils';
|
||||||
|
|
||||||
|
import style from './UploadModal.module.scss';
|
||||||
|
|
||||||
|
interface UploadModalProps {
|
||||||
|
onClose: () => void;
|
||||||
|
isOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UploadModal({ onClose, isOpen }: UploadModalProps) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const { emitError } = useContext(LoggingContext);
|
||||||
|
const [errors, setErrors] = useState<string[]>([]);
|
||||||
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
const [progress, setProgress] = useState(0);
|
||||||
|
const overrideOptionRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const handleFile = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const fileUploaded = event?.target?.files?.[0];
|
||||||
|
if (!fileUploaded) return;
|
||||||
|
|
||||||
|
const validate = validateFile(fileUploaded);
|
||||||
|
setErrors(validate.errors);
|
||||||
|
|
||||||
|
if (validate.isValid) {
|
||||||
|
setFile(fileUploaded);
|
||||||
|
} else {
|
||||||
|
setFile(null);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleUpload = useCallback(async () => {
|
||||||
|
if (file) {
|
||||||
|
try {
|
||||||
|
await uploadEvents(file, setProgress, { onlyEvents: overrideOptionRef?.current?.checked });
|
||||||
|
} catch (error) {
|
||||||
|
emitError(`Failed uploading file: ${error}`);
|
||||||
|
} finally {
|
||||||
|
await queryClient.invalidateQueries(EVENTS_TABLE);
|
||||||
|
setFile(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [emitError, file, queryClient]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
onClose={onClose}
|
||||||
|
isOpen={isOpen}
|
||||||
|
closeOnOverlayClick={false}
|
||||||
|
motionPreset='slideInBottom'
|
||||||
|
size='xl'
|
||||||
|
scrollBehavior='inside'
|
||||||
|
>
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>File upload</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody className={style.modalBody}>
|
||||||
|
<FormControl isInvalid={errors.length > 0}>
|
||||||
|
<FormLabel>Select file to upload</FormLabel>
|
||||||
|
<Input type='file' onChange={handleFile} accept='.json, .xlsx' />
|
||||||
|
{errors.length === 0 ? (
|
||||||
|
<FormHelperText>.XLSX .JSON with max 1MB</FormHelperText>
|
||||||
|
) : (
|
||||||
|
<FormErrorMessage className={style.flexColumnLeft}>
|
||||||
|
{errors.map((error) => (
|
||||||
|
<span key={error}>{error}</span>
|
||||||
|
))}
|
||||||
|
</FormErrorMessage>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
<div className={style.options}>
|
||||||
|
<b>Options</b>
|
||||||
|
<Checkbox ref={overrideOptionRef}>Import only events</Checkbox>
|
||||||
|
<span className={style.notes}>This will prevent overriding user settings</span>
|
||||||
|
</div>
|
||||||
|
{file && (
|
||||||
|
<div className={style.info}>
|
||||||
|
<span>File ready to upload</span>
|
||||||
|
<TooltipActionBtn
|
||||||
|
clickHandler={() => setFile(null)}
|
||||||
|
tooltip='Cancel'
|
||||||
|
aria-label='Cancel'
|
||||||
|
className={style.corner}
|
||||||
|
size='sm'
|
||||||
|
variant='ghosted'
|
||||||
|
icon={<IoCloseSharp />}
|
||||||
|
/>
|
||||||
|
<ul className={style.infoList}>
|
||||||
|
<li>{file.name}</li>
|
||||||
|
<li>{`${(file.size / 1024).toFixed(2)}kb`}</li>
|
||||||
|
<li>{file.type}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Progress value={progress} />
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
colorScheme='blue'
|
||||||
|
disabled={!file || errors.length > 0}
|
||||||
|
onClick={handleUpload}
|
||||||
|
isLoading={progress < 0 && progress >= 100}
|
||||||
|
>
|
||||||
|
Upload
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
type ValidationStatus = {
|
||||||
|
errors: string[];
|
||||||
|
isValid: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function validateFile(file: File): ValidationStatus {
|
||||||
|
const status:ValidationStatus = { errors: [], isValid: true };
|
||||||
|
if (!file) {
|
||||||
|
status.errors.push('No file to upload');
|
||||||
|
status.isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit file size to 1MB
|
||||||
|
if (file.size > 1000000) {
|
||||||
|
status.errors.push('File size limit (1MB) exceeded');
|
||||||
|
status.isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check file extension
|
||||||
|
if (!file.name.endsWith('.xlsx') && !file.name.endsWith('.json')) {
|
||||||
|
status.errors.push('Unhandled file type');
|
||||||
|
status.isValid = false;
|
||||||
|
}
|
||||||
|
return status;
|
||||||
|
}
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
@use '../../../styles/main' as *;
|
|
||||||
|
|
||||||
.entries {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entryTimes {
|
|
||||||
font-family: 'Open Sans', sans-serif;
|
|
||||||
font-size: 1.4vw;
|
|
||||||
|
|
||||||
min-width: 10vw;
|
|
||||||
opacity: 0.9;
|
|
||||||
letter-spacing: 0.035em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entryTitle {
|
|
||||||
align-self: center;
|
|
||||||
@include ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entryPast,
|
|
||||||
.entryNow,
|
|
||||||
.entryFuture {
|
|
||||||
display: flex;
|
|
||||||
font-size: 1.3vw;
|
|
||||||
padding: 0.2vh 1vw;
|
|
||||||
margin-bottom: 1.5vh;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entryPast {
|
|
||||||
font-size: 1.2vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entryPast > .entryTitle {
|
|
||||||
color: $text-gray-disabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entryNow,
|
|
||||||
.entryFuture {
|
|
||||||
color: $text-white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entryFuture {
|
|
||||||
background-color: $bg-gray-950;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entryNow {
|
|
||||||
line-height: 5vh;
|
|
||||||
background-color: $bg-gray-900;
|
|
||||||
border-bottom: 0.5vh solid $ontime-pink;
|
|
||||||
margin-left: -0.5em;
|
|
||||||
padding-left: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.backstageInd {
|
|
||||||
color: $ontime-pink;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: -0.5vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.backstageInd::before {
|
|
||||||
content: '*';
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import style from './TitleSide.module.scss';
|
|
||||||
|
|
||||||
export default function TitleSide(props) {
|
|
||||||
const { type, label, title, subtitle, presenter } = props;
|
|
||||||
|
|
||||||
const titleStyle = type === 'next' ? style.nextTitle : style.nowTitle;
|
|
||||||
const subStyle = type === 'next' ? style.nextSubtitle : style.nowSubtitle;
|
|
||||||
const presStyle = type === 'next' ? style.nextPresenter : style.nowPresenter;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={style.label}>{label}</div>
|
|
||||||
<div className={titleStyle}>{title}</div>
|
|
||||||
<div className={presStyle}>{presenter}</div>
|
|
||||||
<div className={subStyle}>{subtitle}</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
TitleSide.propTypes = {
|
|
||||||
type: PropTypes.oneOf(['now', 'next']),
|
|
||||||
label: PropTypes.string,
|
|
||||||
title: PropTypes.string,
|
|
||||||
subtitle: PropTypes.string,
|
|
||||||
presenter: PropTypes.string,
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
@use '../../../styles/main' as *;
|
|
||||||
|
|
||||||
.label {
|
|
||||||
@include card-label;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nowTitle,
|
|
||||||
.nextTitle {
|
|
||||||
@include card-title;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nowSubtitle,
|
|
||||||
.nowPresenter,
|
|
||||||
.nextSubtitle,
|
|
||||||
.nextPresenter {
|
|
||||||
color: $subtitle-gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nowTitle {
|
|
||||||
font-size: 2.5vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nowPresenter {
|
|
||||||
font-size: 2vw;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nowSubtitle {
|
|
||||||
font-size: 1.2vw;
|
|
||||||
margin-top: -1vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nextTitle {
|
|
||||||
font-size: 2vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nextSubtitle {
|
|
||||||
margin-top: -0.8vh;
|
|
||||||
font-size: 1.1vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nextPresenter {
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 1.5vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nowTitle:after,
|
|
||||||
.nowSubtitle:after,
|
|
||||||
.nowPresenter:after,
|
|
||||||
.nextTitle:after,
|
|
||||||
.nextSubtitle:after,
|
|
||||||
.nextPresenter:after {
|
|
||||||
content: '\200b';
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { stringFromMillis } from '../../utils/time';
|
|
||||||
import style from './Paginator.module.scss';
|
|
||||||
|
|
||||||
export default function TodayItem(props) {
|
|
||||||
const { selected, timeStart, timeEnd, title, backstageEvent, colour } = props;
|
|
||||||
|
|
||||||
// Format timers
|
|
||||||
const start = stringFromMillis(timeStart, false) || '';
|
|
||||||
const end = stringFromMillis(timeEnd, false) || '';
|
|
||||||
|
|
||||||
// user colours
|
|
||||||
const userColour = colour !== '' ? colour : 'transparent';
|
|
||||||
|
|
||||||
// select styling
|
|
||||||
let selectStyle = style.entryPast;
|
|
||||||
if (selected === 1) selectStyle = style.entryNow;
|
|
||||||
else if (selected === 2) selectStyle = style.entryFuture;
|
|
||||||
return (
|
|
||||||
<div className={selectStyle} style={{ borderLeft: `4px solid ${userColour}` }}>
|
|
||||||
<div className={`${style.entryTimes} ${backstageEvent ? style.backstage : ''}`}>
|
|
||||||
{`${start} · ${end}`}
|
|
||||||
</div>
|
|
||||||
<div className={style.entryTitle}>{title}</div>
|
|
||||||
{backstageEvent && <div className={style.backstageInd} />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
TodayItem.propTypes = {
|
|
||||||
selected: PropTypes.number,
|
|
||||||
timeStart: PropTypes.number,
|
|
||||||
timeEnd: PropTypes.number,
|
|
||||||
title: PropTypes.string,
|
|
||||||
backstageEvent: PropTypes.bool,
|
|
||||||
colour: PropTypes.string,
|
|
||||||
};
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, { createContext, useCallback, useEffect, useState } from 'react';
|
import { createContext, useCallback, useEffect, useState } from 'react';
|
||||||
import { useFetch } from '../hooks/useFetch';
|
|
||||||
import { APP_SETTINGS } from '../api/apiConstants';
|
import { APP_SETTINGS } from '../api/apiConstants';
|
||||||
import { getSettings } from '../api/ontimeApi';
|
import { getSettings } from '../api/ontimeApi';
|
||||||
|
import { useFetch } from '../hooks/useFetch';
|
||||||
|
|
||||||
export const AppContext = createContext({
|
export const AppContext = createContext({
|
||||||
auth: false,
|
auth: false,
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { createContext, useCallback } from 'react';
|
import { createContext, useCallback } from 'react';
|
||||||
|
|
||||||
import { useLocalStorage } from '../hooks/useLocalStorage';
|
import { useLocalStorage } from '../hooks/useLocalStorage';
|
||||||
|
|
||||||
export const CollapseContext = createContext({
|
export const CollapseContext = createContext({
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { createContext, useCallback, useMemo, useState } from 'react';
|
import { createContext, useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { useLocalStorage } from '../hooks/useLocalStorage';
|
import { useLocalStorage } from '../hooks/useLocalStorage';
|
||||||
|
|
||||||
export const CursorContext = createContext({
|
export const CursorContext = createContext({
|
||||||
@@ -1,20 +1,47 @@
|
|||||||
import React, { createContext, useCallback, useEffect, useState } from 'react';
|
import { createContext, ReactNode, 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({
|
import { generateId } from '../utils/generate_id';
|
||||||
|
import { nowInMillis, stringFromMillis } from '../utils/time';
|
||||||
|
|
||||||
|
import { useSocket } from './socketContext';
|
||||||
|
|
||||||
|
type LOG_LEVEL = 'INFO' | 'WARN' | 'ERROR';
|
||||||
|
type Log = {
|
||||||
|
id: string;
|
||||||
|
origin: string;
|
||||||
|
time: string;
|
||||||
|
level: LOG_LEVEL;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface LoggingProviderState {
|
||||||
|
logData: Log[];
|
||||||
|
emitInfo: (text: string) => void;
|
||||||
|
emitWarning: (text: string) => void;
|
||||||
|
emitError: (text: string) => void;
|
||||||
|
clearLog: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoggingProviderProps = {
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const notInitialised = () => {
|
||||||
|
throw new Error("Not initialised");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LoggingContext = createContext<LoggingProviderState>({
|
||||||
logData: [],
|
logData: [],
|
||||||
emitInfo: () => undefined,
|
emitInfo: notInitialised,
|
||||||
emitWarning: () => undefined,
|
emitWarning: notInitialised,
|
||||||
emitError: () => undefined,
|
emitError: notInitialised,
|
||||||
clearLog: () => undefined,
|
clearLog: notInitialised
|
||||||
});
|
});
|
||||||
|
|
||||||
export const LoggingProvider = ({ children }) => {
|
export const LoggingProvider = ({ children }: LoggingProviderProps) => {
|
||||||
const MAX_MESSAGES = 100;
|
const MAX_MESSAGES = 100;
|
||||||
const socket = useSocket();
|
const socket = useSocket();
|
||||||
const [logData, setLogData] = useState([]);
|
const [logData, setLogData] = useState<Log[]>([]);
|
||||||
const origin = 'USER';
|
const origin = 'USER';
|
||||||
|
|
||||||
// handle incoming messages
|
// handle incoming messages
|
||||||
@@ -24,7 +51,7 @@ export const LoggingProvider = ({ children }) => {
|
|||||||
// Ask for log data
|
// Ask for log data
|
||||||
socket.emit('get-logger');
|
socket.emit('get-logger');
|
||||||
|
|
||||||
socket.on('logger', (data) => {
|
socket.on('logger', (data: Log) => {
|
||||||
setLogData((l) => [data, ...l]);
|
setLogData((l) => [data, ...l]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -41,9 +68,9 @@ export const LoggingProvider = ({ children }) => {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
const _send = useCallback(
|
const _send = useCallback(
|
||||||
(text, level) => {
|
(text: string, level: LOG_LEVEL) => {
|
||||||
if (socket != null) {
|
if (socket != null) {
|
||||||
const m = {
|
const m: Log = {
|
||||||
id: generateId(),
|
id: generateId(),
|
||||||
origin,
|
origin,
|
||||||
time: stringFromMillis(nowInMillis()),
|
time: stringFromMillis(nowInMillis()),
|
||||||
@@ -54,7 +81,7 @@ export const LoggingProvider = ({ children }) => {
|
|||||||
socket.emit('logger', m);
|
socket.emit('logger', m);
|
||||||
}
|
}
|
||||||
if (logData.length > MAX_MESSAGES) {
|
if (logData.length > MAX_MESSAGES) {
|
||||||
setLogData((l) => l.pop());
|
setLogData((l) => l.slice(1));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[logData, socket]
|
[logData, socket]
|
||||||
@@ -65,7 +92,7 @@ export const LoggingProvider = ({ children }) => {
|
|||||||
* @param text
|
* @param text
|
||||||
*/
|
*/
|
||||||
const emitInfo = useCallback(
|
const emitInfo = useCallback(
|
||||||
(text) => {
|
(text: string) => {
|
||||||
_send(text, 'INFO');
|
_send(text, 'INFO');
|
||||||
},
|
},
|
||||||
[_send]
|
[_send]
|
||||||
@@ -76,7 +103,7 @@ export const LoggingProvider = ({ children }) => {
|
|||||||
* @param text
|
* @param text
|
||||||
*/
|
*/
|
||||||
const emitWarning = useCallback(
|
const emitWarning = useCallback(
|
||||||
(text) => {
|
(text: string) => {
|
||||||
_send(text, 'WARN');
|
_send(text, 'WARN');
|
||||||
},
|
},
|
||||||
[_send]
|
[_send]
|
||||||
@@ -87,7 +114,7 @@ export const LoggingProvider = ({ children }) => {
|
|||||||
* @param text
|
* @param text
|
||||||
*/
|
*/
|
||||||
const emitError = useCallback(
|
const emitError = useCallback(
|
||||||
(text) => {
|
(text: string) => {
|
||||||
_send(text, 'ERROR');
|
_send(text, 'ERROR');
|
||||||
},
|
},
|
||||||
[_send]
|
[_send]
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { createContext, useCallback, useState } from 'react';
|
import { createContext, useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { useLocalStorage } from '../hooks/useLocalStorage';
|
import { useLocalStorage } from '../hooks/useLocalStorage';
|
||||||
|
|
||||||
export const TableSettingsContext = createContext({
|
export const TableSettingsContext = createContext({
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { createContext, ReactNode, useContext, useEffect, useState } from 'react';
|
||||||
|
import { serverURL } from 'common/api/apiConstants';
|
||||||
|
import io, { Socket } from 'socket.io-client';
|
||||||
|
|
||||||
|
interface SocketProviderState {
|
||||||
|
socket: Socket | null;
|
||||||
|
emit: <T>(topic: string, payload?: T) => void;
|
||||||
|
on: <T>(topic: string, callback: (data: T) => void) => void;
|
||||||
|
off: (topic: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type SocketProviderProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SocketContext = createContext<SocketProviderState>({
|
||||||
|
socket: null,
|
||||||
|
emit: () => {},
|
||||||
|
on: () => {},
|
||||||
|
off: () => {}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useSocket = () => {
|
||||||
|
return useContext(SocketContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
function SocketProvider({ children }: SocketProviderProps) {
|
||||||
|
const [socket, setSocket] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const socketInstance = io(serverURL, { transports: ["websocket"] });
|
||||||
|
setSocket(socketInstance);
|
||||||
|
return () => {
|
||||||
|
socketInstance.disconnect();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SocketContext.Provider value={socket}>{children}</SocketContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SocketProvider;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
export default function useElectronEvent() {
|
||||||
|
const isElectron = window?.process?.type === 'renderer';
|
||||||
|
|
||||||
|
const sendToElectron = (channel: string, args: any) => {
|
||||||
|
if (isElectron) {
|
||||||
|
window?.ipcRenderer.send(channel, args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { isElectron, sendToElectron };
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useQuery } from 'react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
const refetchIntervalMs = 10000;
|
const refetchIntervalMs = 10000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -10,7 +11,6 @@ export const useFetch = (namespace, fn) => {
|
|||||||
const { data, status, isError, refetch } = useQuery(namespace, fn, {
|
const { data, status, isError, refetch } = useQuery(namespace, fn, {
|
||||||
refetchInterval: refetchIntervalMs,
|
refetchInterval: refetchIntervalMs,
|
||||||
cacheTime: Infinity,
|
cacheTime: Infinity,
|
||||||
notifyOnChangeProps: 'tracked',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return { data, status, isError, refetch };
|
return { data, status, isError, refetch };
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
import {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
export type TLogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';
|
||||||
|
|
||||||
|
export type TOptions = {
|
||||||
|
logLevel?: TLogLevel;
|
||||||
|
maxFontSize?: number;
|
||||||
|
minFontSize?: number;
|
||||||
|
onFinish?: (fontSize: number) => void;
|
||||||
|
onStart?: () => void;
|
||||||
|
resolution?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LOG_LEVEL: Record<TLogLevel, number> = {
|
||||||
|
debug: 10,
|
||||||
|
info: 20,
|
||||||
|
warn: 30,
|
||||||
|
error: 40,
|
||||||
|
none: 100,
|
||||||
|
};
|
||||||
|
|
||||||
|
const useFitText = ({
|
||||||
|
logLevel: logLevelOption = 'info',
|
||||||
|
maxFontSize = 100,
|
||||||
|
minFontSize = 20,
|
||||||
|
onFinish,
|
||||||
|
onStart,
|
||||||
|
resolution = 5,
|
||||||
|
}: TOptions = {}) => {
|
||||||
|
const logLevel = LOG_LEVEL[logLevelOption];
|
||||||
|
|
||||||
|
const initState = useCallback(() => {
|
||||||
|
return {
|
||||||
|
calcKey: 0,
|
||||||
|
fontSize: maxFontSize,
|
||||||
|
fontSizePrev: minFontSize,
|
||||||
|
fontSizeMax: maxFontSize,
|
||||||
|
fontSizeMin: minFontSize,
|
||||||
|
};
|
||||||
|
}, [maxFontSize, minFontSize]);
|
||||||
|
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const innerHtmlPrevRef = useRef<string | null>();
|
||||||
|
const isCalculatingRef = useRef(false);
|
||||||
|
const [state, setState] = useState(initState);
|
||||||
|
const { calcKey, fontSize, fontSizeMax, fontSizeMin, fontSizePrev } = state;
|
||||||
|
|
||||||
|
// Monitor div size changes and recalculate on resize
|
||||||
|
let animationFrameId: number | null = null;
|
||||||
|
const [ro] = useState(
|
||||||
|
() =>
|
||||||
|
new ResizeObserver(() => {
|
||||||
|
animationFrameId = window.requestAnimationFrame(() => {
|
||||||
|
if (isCalculatingRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onStart && onStart();
|
||||||
|
isCalculatingRef.current = true;
|
||||||
|
// `calcKey` is used in the dependencies array of
|
||||||
|
// `useIsoLayoutEffect` below. It is incremented so that the font size
|
||||||
|
// will be recalculated even if the previous state didn't change (e.g.
|
||||||
|
// when the text fit initially).
|
||||||
|
setState({
|
||||||
|
...initState(),
|
||||||
|
calcKey: calcKey + 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (ref.current) {
|
||||||
|
ro.observe(ref.current);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
animationFrameId && window.cancelAnimationFrame(animationFrameId);
|
||||||
|
ro.disconnect();
|
||||||
|
};
|
||||||
|
}, [animationFrameId, ro]);
|
||||||
|
|
||||||
|
// Recalculate when the div contents change
|
||||||
|
const innerHtml = ref.current && ref.current.innerHTML;
|
||||||
|
useEffect(() => {
|
||||||
|
if (calcKey === 0 || isCalculatingRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (innerHtml !== innerHtmlPrevRef.current) {
|
||||||
|
onStart && onStart();
|
||||||
|
setState({
|
||||||
|
...initState(),
|
||||||
|
calcKey: calcKey + 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
innerHtmlPrevRef.current = innerHtml;
|
||||||
|
}, [calcKey, initState, innerHtml, onStart]);
|
||||||
|
|
||||||
|
// Check overflow and resize font
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
// Don't start calculating font size until the `resizeKey` is incremented
|
||||||
|
// above in the `ResizeObserver` callback. This avoids an extra resize
|
||||||
|
// on initialization.
|
||||||
|
if (calcKey === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isWithinResolution = Math.abs(fontSize - fontSizePrev) <= resolution;
|
||||||
|
const isOverflow =
|
||||||
|
!!ref.current &&
|
||||||
|
(ref.current.scrollHeight > ref.current.offsetHeight ||
|
||||||
|
ref.current.scrollWidth > ref.current.offsetWidth);
|
||||||
|
const isFailed = isOverflow && fontSize === fontSizePrev;
|
||||||
|
const isAsc = fontSize > fontSizePrev;
|
||||||
|
|
||||||
|
// Return if the font size has been adjusted "enough" (change within `resolution`)
|
||||||
|
// reduce font size by one increment if it's overflowing.
|
||||||
|
if (isWithinResolution) {
|
||||||
|
if (isFailed) {
|
||||||
|
isCalculatingRef.current = false;
|
||||||
|
if (logLevel <= LOG_LEVEL.info) {
|
||||||
|
console.info(
|
||||||
|
`[use-fit-text] reached \`minFontSize = ${minFontSize}\` without fitting text`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (isOverflow) {
|
||||||
|
setState({
|
||||||
|
fontSize: isAsc ? fontSizePrev : fontSizeMin,
|
||||||
|
fontSizeMax,
|
||||||
|
fontSizeMin,
|
||||||
|
fontSizePrev,
|
||||||
|
calcKey,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
isCalculatingRef.current = false;
|
||||||
|
onFinish && onFinish(fontSize);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Binary search to adjust font size
|
||||||
|
let delta: number;
|
||||||
|
let newMax = fontSizeMax;
|
||||||
|
let newMin = fontSizeMin;
|
||||||
|
if (isOverflow) {
|
||||||
|
delta = isAsc ? fontSizePrev - fontSize : fontSizeMin - fontSize;
|
||||||
|
newMax = Math.min(fontSizeMax, fontSize);
|
||||||
|
} else {
|
||||||
|
delta = isAsc ? fontSizeMax - fontSize : fontSizePrev - fontSize;
|
||||||
|
newMin = Math.max(fontSizeMin, fontSize);
|
||||||
|
}
|
||||||
|
setState({
|
||||||
|
calcKey,
|
||||||
|
fontSize: fontSize + delta / 2,
|
||||||
|
fontSizeMax: newMax,
|
||||||
|
fontSizeMin: newMin,
|
||||||
|
fontSizePrev: fontSize,
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
calcKey,
|
||||||
|
fontSize,
|
||||||
|
fontSizeMax,
|
||||||
|
fontSizeMin,
|
||||||
|
fontSizePrev,
|
||||||
|
onFinish,
|
||||||
|
ref,
|
||||||
|
resolution,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { fontSize: `${fontSize}%`, ref };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useFitText;
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
|
||||||
|
export default function useFullscreen() {
|
||||||
|
const [isFullScreen, setFullScreen] = useState(document.fullscreenElement);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleChange = () => {
|
||||||
|
setFullScreen(document.fullscreenElement);
|
||||||
|
};
|
||||||
|
document.addEventListener('fullscreenchange', handleChange, { passive: true });
|
||||||
|
document.addEventListener('resize', handleChange, { passive: true });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('fullscreenchange', handleChange, { passive: true });
|
||||||
|
document.removeEventListener('resize', handleChange, { passive: true });
|
||||||
|
}; }, []);
|
||||||
|
|
||||||
|
const toggleFullScreen = useCallback(() => {
|
||||||
|
if (!document.fullscreenElement) {
|
||||||
|
document.documentElement.requestFullscreen();
|
||||||
|
} else {
|
||||||
|
if (document.exitFullscreen) {
|
||||||
|
document.exitFullscreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setFullScreen(document.fullscreenElement);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { isFullScreen, toggleFullScreen };
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useMutation, useQueryClient } from 'react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { EVENTS_TABLE } from '../api/apiConstants';
|
import { EVENTS_TABLE } from '../api/apiConstants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
const scriptTagId = 'ontime-override';
|
||||||
|
export const useRuntimeStylesheet = (pathToFile) => {
|
||||||
|
const [shouldRender, setShouldRender] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
let response = await fetch(pathToFile);
|
||||||
|
if (response.ok) {
|
||||||
|
return await response.text();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!pathToFile) {
|
||||||
|
document.getElementById(scriptTagId)?.remove();
|
||||||
|
setShouldRender(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.getElementById(scriptTagId)) {
|
||||||
|
setShouldRender(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setShouldRender(false);
|
||||||
|
const styleSheet = document.createElement('style');
|
||||||
|
styleSheet.rel = 'stylesheet';
|
||||||
|
styleSheet.setAttribute('id', scriptTagId);
|
||||||
|
|
||||||
|
fetchData()
|
||||||
|
.then((data) => {
|
||||||
|
styleSheet.innerHTML = data;
|
||||||
|
document.head.append(styleSheet);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(`Error loading stylesheet: ${error}`);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
// schedule render for next tick
|
||||||
|
setTimeout(() => setShouldRender(true), 0);
|
||||||
|
});
|
||||||
|
}, [pathToFile]);
|
||||||
|
|
||||||
|
return { shouldRender };
|
||||||
|
};
|
||||||