diff --git a/client/package.json b/client/package.json index 7247c1ad6..4485a3dce 100644 --- a/client/package.json +++ b/client/package.json @@ -3,29 +3,29 @@ "version": "1.8.2", "private": true, "dependencies": { - "@chakra-ui/react": "^2.3.2", + "@chakra-ui/react": "2.3.4", "@dnd-kit/core": "^6.0.5", "@dnd-kit/sortable": "^7.0.1", "@dnd-kit/utilities": "^3.2.0", "@emotion/react": "^11.10.4", "@emotion/styled": "^11.10.4", "@react-icons/all-files": "^4.1.0", - "@tanstack/react-query": "^4.1.3", - "@tanstack/react-query-devtools": "^4.0.10", + "@tanstack/react-query": "^4.10.3", + "@tanstack/react-query-devtools": "^4.11.0", "autosize": "^5.0.1", "axios": "^0.27.2", "color": "^4.2.3", - "framer-motion": "^7.3.2", - "jotai": "^1.7.8", + "framer-motion": "^7.5.3", + "jotai": "^1.8.5", "luxon": "^3.0.1", - "react": "^18.1.0", - "react-beautiful-dnd": "^13.1.0", - "react-dom": "^18.1.0", + "react": "^18.2.0", + "react-beautiful-dnd": "^13.1.1", + "react-dom": "^18.2.0", "react-fast-compare": "^3.2.0", "react-qr-code": "^2.0.5", "react-router-dom": "^6.3.0", "react-table": "^7.7.0", - "socket.io-client": "^4.5.1", + "socket.io-client": "^4.5.2", "typeface-open-sans": "^1.1.13", "web-vitals": "^2.1.4" }, @@ -56,12 +56,13 @@ "@testing-library/user-event": "^14.1.1", "@types/color": "^3.0.3", "@types/node": "^18.7.16", - "@types/react": "^18.0.19", + "@types/react": "^18.0.21", + "@types/react-beautiful-dnd": "^13.1.2", "@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": "^8.25.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-jest": "^27.0.4", "eslint-plugin-react": "^7.31.8", @@ -76,9 +77,12 @@ "stylelint-config-prettier": "^9.0.3", "stylelint-config-standard-scss": "^4.0.0", "typescript": "^4.8.3", - "vite": "^3.1.1", + "vite": "^3.1.6", "vite-plugin-svgr": "^2.2.1", "vite-tsconfig-paths": "^3.5.0", "vitest": "^0.23.2" + }, + "resolutions": { + "**/@types/react": "18.0.21" } -} \ No newline at end of file +} diff --git a/client/src/App.jsx b/client/src/App.jsx index 81ebca9b5..1fa66ff09 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1,18 +1,19 @@ import { Suspense, useCallback, useEffect } from 'react'; import { BrowserRouter } from 'react-router-dom'; import { ChakraProvider } from '@chakra-ui/react'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import ErrorBoundary from 'common/components/errorBoundary/ErrorBoundary'; +import { AppContextProvider } from 'common/context/AppContext'; +import { LoggingProvider } from 'common/context/LoggingContext'; +import SocketProvider from 'common/context/socketContext'; -import { AppContextProvider } from './common/context/AppContext'; -import SocketProvider from './common/context/socketContext'; +import { ontimeQueryClient } from './common/queryClient'; import theme from './theme/theme'; import AppRouter from './AppRouter'; // Load Open Sans typeface import('typeface-open-sans'); -export const ontimeQueryClient = new QueryClient(); function App() { @@ -45,20 +46,22 @@ function App() { return ( - - - -
- - - - - - -
-
-
-
+ + + + +
+ + + + + + +
+
+
+
+
); diff --git a/client/src/AppRouter.jsx b/client/src/AppRouter.jsx index c44168e4b..be76b694c 100644 --- a/client/src/AppRouter.jsx +++ b/client/src/AppRouter.jsx @@ -1,10 +1,8 @@ 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 useAliases from './common/hooks-query/useAliases'; import withSocket from './features/viewers/ViewWrapper'; const Editor = lazy(() => import('features/editors/ProtectedEditor')); @@ -39,7 +37,7 @@ const Info = lazy(() => import('features/info/InfoExport')); export default function AppRouter() { useSocketProvider(); - const { data } = useFetch(ALIASES, getAliases); + const { data } = useAliases(); const location = useLocation(); const navigate = useNavigate(); diff --git a/client/src/appConstants.js b/client/src/appConstants.js index 72c1578b2..7fc7179fe 100644 --- a/client/src/appConstants.js +++ b/client/src/appConstants.js @@ -6,13 +6,19 @@ const publicLocation = 'public'; const pipLocation = 'pip'; const studioLocation = 'studio'; const cuesheetLocation = 'cuesheet'; +const countdownLocation = 'countdown'; +const clockLocation = 'clock'; +const lowerLocation = 'lower'; export const viewerLocations = [ + { link: speakerLocation, label: 'Stage timer' }, + { link: clockLocation, label: 'Clock' }, { link: minimalLocation, label: 'Minimal timer' }, - { link: speakerLocation, label: 'Speaker Screen' }, - { link: smLocation, label: 'Backstage Screen' }, - { link: publicLocation, label: 'Public Screen' }, + { link: smLocation, label: 'Backstage screen' }, + { link: publicLocation, label: 'Public screen' }, + { link: lowerLocation, label: 'Lower thirds' }, { link: pipLocation, label: 'Picture in Picture' }, - { link: studioLocation, label: 'Studio Clock' }, - { link: cuesheetLocation, label: 'Cuesheet' } -]; \ No newline at end of file + { link: studioLocation, label: 'Studio clock' }, + { link: countdownLocation, label: 'Countdown' }, + { link: cuesheetLocation, label: 'Cuesheet' }, +]; diff --git a/client/src/common/api/apiConstants.js b/client/src/common/api/apiConstants.js index f7727676c..06aecb03b 100644 --- a/client/src/common/api/apiConstants.js +++ b/client/src/common/api/apiConstants.js @@ -2,8 +2,9 @@ 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 EVENTS_TABLE_KEY = 'events'; +export const EVENTS_TABLE = [EVENTS_TABLE_KEY]; +export const APP_INFO = ['appinfo']; export const OSC_SETTINGS = ['oscSettings']; export const APP_SETTINGS = ['appSettings']; export const VIEW_SETTINGS = ['viewSettings']; @@ -25,7 +26,6 @@ export const calculateServer = () => export const serverURL = calculateServer(); export const eventURL = `${serverURL}/${EVENT_TABLE}`; export const eventsURL = `${serverURL}/${EVENTS_TABLE}`; -export const playbackURL = `${serverURL}/playback`; export const ontimeURL = `${serverURL}/ontime`; export const stylesPath = 'external/styles/override.css'; diff --git a/client/src/common/api/eventApi.js b/client/src/common/api/eventApi.ts similarity index 57% rename from client/src/common/api/eventApi.js rename to client/src/common/api/eventApi.ts index d59335b7c..f7d0490e1 100644 --- a/client/src/common/api/eventApi.js +++ b/client/src/common/api/eventApi.ts @@ -1,18 +1,22 @@ import axios from 'axios'; +import { EventDataType } from '../models/EventData.type'; + import { eventURL } from './apiConstants'; /** * @description HTTP request to fetch event data * @return {Promise} */ -export const fetchEvent = async () => { +export async function fetchEvent(): Promise { const res = await axios.get(eventURL); return res.data; -}; +} /** * @description HTTP request to mutate event data * @return {Promise} */ -export const postEvent = async (data) => axios.post(eventURL, data); +export async function postEvent(data: EventDataType) { + return axios.post(eventURL, data); +} diff --git a/client/src/common/api/eventsApi.js b/client/src/common/api/eventsApi.js deleted file mode 100644 index 9e56d4f9a..000000000 --- a/client/src/common/api/eventsApi.js +++ /dev/null @@ -1,54 +0,0 @@ -import axios from 'axios'; - -import { eventsURL } from './apiConstants'; - -/** - * @description HTTP request to fetch all events - * @return {Promise} - */ -export const fetchAllEvents = async () => { - const res = await axios.get(eventsURL); - return res.data; -}; - -/** - * @description HTTP request to post new event - * @return {Promise} - */ -export const requestPost = async (data) => axios.post(eventsURL, data); - -/** - * @description HTTP request to put new event - * @return {Promise} - */ -export const requestPut = async (data) => axios.put(eventsURL, data); - -/** - * @description HTTP request to modify event - * @return {Promise} - */ -export const requestPatch = async (data) => axios.patch(eventsURL, data); - -/** - * @description HTTP request to reorder events - * @return {Promise} - */ -export const requestReorder = async (data) => axios.patch(`${eventsURL}/reorder`, data); - -/** - * @description HTTP request to request application of delay - * @return {Promise} - */ -export const requestApplyDelay = async (eventId) => axios.patch(`${eventsURL}/applydelay/${eventId}`); - -/** - * @description HTTP request to delete given event - * @return {Promise} - */ -export const requestDelete = async (eventId) => axios.delete(`${eventsURL}/${eventId}`); - -/** - * @description HTTP request to delete all events - * @return {Promise} - */ -export const requestDeleteAll = async () => axios.delete(`${eventsURL}/all`); diff --git a/client/src/common/api/eventsApi.ts b/client/src/common/api/eventsApi.ts new file mode 100644 index 000000000..f9f17fc21 --- /dev/null +++ b/client/src/common/api/eventsApi.ts @@ -0,0 +1,70 @@ +import axios from 'axios'; + +import { OntimeEventEntry } from '../models/EventTypes'; + +import { eventsURL } from './apiConstants'; + +/** + * @description HTTP request to fetch all events + * @return {Promise} + */ +export async function fetchAllEvents(): Promise { + const res = await axios.get(eventsURL); + return res.data; +} + +/** + * @description HTTP request to post new event + * @return {Promise} + */ +export async function requestPostEvent(data: OntimeEventEntry) { + return axios.post(eventsURL, data); +} + +/** + * @description HTTP request to put new event + * @return {Promise} + */ +export async function requestPutEvent(data: OntimeEventEntry) { + return axios.put(eventsURL, data); +} + +/** + * @description HTTP request to modify event + * @return {Promise} + */ +export async function requestPatchEvent(data: OntimeEventEntry) { + return axios.patch(eventsURL, data); +} + +/** + * @description HTTP request to reorder events + * @return {Promise} + */ +export async function requestReorderEvent(data: OntimeEventEntry) { + return axios.patch(`${eventsURL}/reorder`, data); +} + +/** + * @description HTTP request to request application of delay + * @return {Promise} + */ +export async function requestApplyDelay(eventId: string) { + return axios.patch(`${eventsURL}/applydelay/${eventId}`); +} + +/** + * @description HTTP request to delete given event + * @return {Promise} + */ +export async function requestDelete(eventId: string) { + return axios.delete(`${eventsURL}/${eventId}`); +} + +/** + * @description HTTP request to delete all events + * @return {Promise} + */ +export async function requestDeleteAll() { + return axios.delete(`${eventsURL}/all`); +} diff --git a/client/src/common/api/ontimeApi.js b/client/src/common/api/ontimeApi.js deleted file mode 100644 index 9e626e1c1..000000000 --- a/client/src/common/api/ontimeApi.js +++ /dev/null @@ -1,283 +0,0 @@ -import axios from 'axios'; - -import { ontimeURL } from './apiConstants'; - -/** - * @description placeholder information for ontimeInfo - * @type {{settings: {serverPort: number, version: string}, networkInterfaces: *[]}} - */ -export const ontimePlaceholderInfo = { - networkInterfaces: [], - settings: { - version: '', - serverPort: 4001, - }, -}; - -/** - * @description placeholder information for ontimeSettings - * @type {{pinCode: null}} - */ -export const ontimePlaceholderSettings = { - app: 'ontime', - version: 1, - serverPort: 4001, - lock: null, - pinCode: null, - timeFormat: '24', -}; - -/** - * @description placeholder information for eventSettings - * @type {{backstageInfo: string, endMessage: string, publicInfo: string, title: string, url: string}} - */ -export const eventPlaceholderSettings = { - title: '', - url: '', - publicInfo: '', - backstageInfo: '', - endMessage: '', -}; - -/** - * @description placeholder information for userFields - * @type {{user1: string, user2: string, user0: string, user9: string, user7: string, user8: string, user5: string, user6: string, user3: string, user4: string}} - */ -export const userFieldsPlaceholder = { - user0: '', - user1: '', - user2: '', - user3: '', - user4: '', - user5: '', - user6: '', - user7: '', - user8: '', - user9: '', -}; - -/** - * @description placeholder information for oscSettings - * @type {{targetIP: string, port: string, portOut: string, enabled: boolean}} - */ -export const oscPlaceholderSettings = { - port: '', - portOut: '', - targetIP: '', - enabled: false, -}; - -/** - * @description placeholder information for view settings - * @type {{overrideCSS: boolean}} - */ -export const viewsPlaceholder = { - overrideStyles: false, -}; - -/** - * @description placeholder information for httpSettings - * @type {{onStart: {url: string, enabled: boolean}, onLoad: {url: string, enabled: boolean}, onPause: {url: string, enabled: boolean}, onFinish: {url: string, enabled: boolean}, onUpdate: {url: string, enabled: boolean}, onStop: {url: string, enabled: boolean}}} - */ -export const httpPlaceholder = { - onLoad: { - url: '', - enabled: false, - }, - onStart: { - url: '', - enabled: false, - }, - onUpdate: { - url: '', - enabled: false, - }, - onPause: { - url: '', - enabled: false, - }, - onStop: { - url: '', - enabled: false, - }, - onFinish: { - url: '', - enabled: false, - }, -}; - -/** - * @description ontime utility variables - * @type {[{name: string, description: string}, {name: string, description: string}, {name: string, description: string}, {name: string, description: string}, {name: string, description: string}, null, null]} - */ -export const ontimeVars = [ - { - name: '$timer', - description: 'Current running timer', - }, - { - name: '$title', - description: 'Current title', - }, - { - name: '$presenter', - description: 'Current timer', - }, - { - name: '$subtitle', - description: 'Current subtitle', - }, - { - name: '$next-title', - description: 'Next title', - }, - { - name: '$next-presenter', - description: 'Next timer', - }, - { - name: '$next-subtitle', - description: 'Next subtitle', - }, -]; - -/** - * @description HTTP request to retrieve application settings - * @return {Promise} - */ -export const getSettings = async () => { - const res = await axios.get(`${ontimeURL}/settings`); - return res.data; -}; - -/** - * @description HTTP request to mutate application settings - * @return {Promise} - */ -export const postSettings = async (data) => axios.post(`${ontimeURL}/settings`, data); - -/** - * @description HTTP request to retrieve application info - * @return {Promise} - */ -export const getInfo = async () => { - const res = await axios.get(`${ontimeURL}/info`); - return res.data; -}; - -/** - * @description HTTP request to 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 - * @return {Promise} - */ -export const getAliases = async () => { - const res = await axios.get(`${ontimeURL}/aliases`); - return res.data; -}; - -/** - * @description HTTP request to mutate aliases - * @return {Promise} - */ -export const postAliases = async (data) => axios.post(`${ontimeURL}/aliases`, data); - -/** - * @description HTTP request to retrieve user fields - * @return {Promise} - */ -export const getUserFields = async () => { - const res = await axios.get(`${ontimeURL}/userfields`); - return res.data; -}; - -/** - * @description HTTP request to mutate user fields - * @return {Promise} - */ -export const postUserFields = async (data) => axios.post(`${ontimeURL}/userfields`, data); - -/** - * @description HTTP request to retrieve osc settings - * @return {Promise} - */ -export const getOSC = async () => { - const res = await axios.get(`${ontimeURL}/osc`); - return res.data; -}; - -/** - * @description HTTP request to mutate osc settings - * @return {Promise} - */ -export const postOSC = async (data) => axios.post(`${ontimeURL}/osc`, data); - -/** - * @description HTTP request to download db - * @return {Promise} - */ -export const downloadEvents = async () => { - await axios({ - url: `${ontimeURL}/db`, - method: 'GET', - responseType: 'blob', // important - }).then((response) => { - const headerLine = response.headers['Content-Disposition']; - let filename = 'events.json'; - - // try and get the filename from the response - if (headerLine != null) { - const startFileNameIndex = headerLine.indexOf('"') + 1; - const endFileNameIndex = headerLine.lastIndexOf('"'); - filename = headerLine.substring(startFileNameIndex, endFileNameIndex); - } - - const url = window.URL.createObjectURL(new Blob([response.data], { type: 'application/json' })); - const link = document.createElement('a'); - link.href = url; - link.setAttribute('download', filename); - document.body.appendChild(link); - link.click(); - }); -}; - -/** - * @description HTTP request to upload events db - * @return {Promise} - */ -export const uploadEvents = async (file, setProgress, options) => { - const formData = new FormData(); - formData.append('userFile', file); - const onlyEvents = options?.onlyEvents; - await axios - .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 - * @return {Promise} - */ -export const uploadEventsWithPath = async (filepath) => - axios.post(`${ontimeURL}/dbpath`, { path: filepath }); diff --git a/client/src/common/api/ontimeApi.ts b/client/src/common/api/ontimeApi.ts new file mode 100644 index 000000000..a4dba43a1 --- /dev/null +++ b/client/src/common/api/ontimeApi.ts @@ -0,0 +1,157 @@ +import axios from 'axios'; + +import { URLAliasType } from '../models/Alias.type'; +import { InfoType } from '../models/Info.types'; +import { OntimeSettingsType } from '../models/OntimeSettings.type'; +import { OscSettingsType } from '../models/OscSettings.type'; +import { UserFieldsType } from '../models/UserFields.type'; +import { ViewSettingsType } from '../models/ViewSettings.type'; + +import { ontimeURL } from './apiConstants'; + +/** + * @description HTTP request to retrieve application settings + * @return {Promise} + */ +export async function getSettings(): Promise { + const res = await axios.get(`${ontimeURL}/settings`); + return res.data; +} + +/** + * @description HTTP request to mutate application settings + * @return {Promise} + */ +export async function postSettings(data: OntimeSettingsType) { + return axios.post(`${ontimeURL}/settings`, data); +} + +/** + * @description HTTP request to retrieve application info + * @return {Promise} + */ +export async function getInfo(): Promise { + const res = await axios.get(`${ontimeURL}/info`); + return res.data; +} + +/** + * @description HTTP request to retrieve view settings + * @return {Promise} + */ +export async function getView(): Promise { + const res = await axios.get(`${ontimeURL}/views`); + return res.data; +} + +/** + * @description HTTP request to mutate view settings + * @return {Promise} + */ +export async function postView(data: ViewSettingsType) { + return axios.post(`${ontimeURL}/views`, data); +} + +/** + * @description HTTP request to retrieve aliases + * @return {Promise} + */ +export async function getAliases(): Promise { + const res = await axios.get(`${ontimeURL}/aliases`); + return res.data; +} + +/** + * @description HTTP request to mutate aliases + * @return {Promise} + */ +export async function postAliases(data: URLAliasType[]) { + return axios.post(`${ontimeURL}/aliases`, data); +} + +/** + * @description HTTP request to retrieve user fields + * @return {Promise} + */ +export async function getUserFields(): Promise { + const res = await axios.get(`${ontimeURL}/userfields`); + return res.data; +} + +/** + * @description HTTP request to mutate user fields + * @return {Promise} + */ +export async function postUserFields(data: UserFieldsType) { + return axios.post(`${ontimeURL}/userfields`, data); +} + +/** + * @description HTTP request to retrieve osc settings + * @return {Promise} + */ +export async function getOSC(): Promise { + const res = await axios.get(`${ontimeURL}/osc`); + return res.data; +} + +/** + * @description HTTP request to mutate osc settings + * @return {Promise} + */ +export async function postOSC(data: OscSettingsType) { + return axios.post(`${ontimeURL}/osc`, data); +} + +/** + * @description HTTP request to download db + * @return {Promise} + */ +export const downloadEvents = async () => { + await axios({ + url: `${ontimeURL}/db`, + method: 'GET', + responseType: 'blob', // important + }).then((response) => { + const headerLine = response.headers['Content-Disposition']; + let filename = 'events.json'; + + // try and get the filename from the response + if (headerLine != null) { + const startFileNameIndex = headerLine.indexOf('"') + 1; + const endFileNameIndex = headerLine.lastIndexOf('"'); + filename = headerLine.substring(startFileNameIndex, endFileNameIndex); + } + + const url = window.URL.createObjectURL(new Blob([response.data], { type: 'application/json' })); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', filename); + document.body.appendChild(link); + link.click(); + }); +}; + +/** + * @description HTTP request to upload events db + * @return {Promise} + */ +type UploadEventsOptions = { + onlyEvents?: boolean; +} +export const uploadEvents = async (file: string, setProgress: (value: number) => void, options?: UploadEventsOptions) => { + const formData = new FormData(); + formData.append('userFile', file); + const onlyEvents = options?.onlyEvents; + await axios + .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); +}; diff --git a/client/src/common/api/playbackApi.js b/client/src/common/api/playbackApi.js deleted file mode 100644 index 59f3b3a32..000000000 --- a/client/src/common/api/playbackApi.js +++ /dev/null @@ -1,45 +0,0 @@ -import axios from 'axios'; - -import { playbackURL } from './apiConstants'; - -/** - * @description HTTP call to start current timer - * @return {Promise} - */ -export const getStart = async () => axios.get(`${playbackURL}/start`); - -/** - * @description HTTP call to pause current timer - * @return {Promise} - */ -export const getPause = async () => axios.get(`${playbackURL}/pause`); - -/** - * @description HTTP call to start roll mode - * @return {Promise} - */ -export const getRoll = async () => axios.get(`${playbackURL}/roll`); - -/** - * @description HTTP call to skip to previous event - * @return {Promise} - */ -export const getPrevious = async () => axios.get(`${playbackURL}/previous`); - -/** - * @description HTTP call to skip to next event - * @return {Promise} - */ -export const getNext = async () => axios.get(`${playbackURL}/next`); - -/** - * @description HTTP call to unload current timer - * @return {Promise} - */ -export const getUnload = async () => axios.get(`${playbackURL}/unload`); - -/** - * @description HTTP call to reload current timer - * @return {Promise} - */ -export const getReload = async () => axios.get(`${playbackURL}/reload`); diff --git a/client/src/common/atoms/LocalEventSettings.ts b/client/src/common/atoms/LocalEventSettings.ts index 9e02d372a..ca8efbb22 100644 --- a/client/src/common/atoms/LocalEventSettings.ts +++ b/client/src/common/atoms/LocalEventSettings.ts @@ -1,3 +1,4 @@ +import { atom } from 'jotai'; import { atomWithStorage, selectAtom } from 'jotai/utils'; export const eventSettingsAtom = atomWithStorage('ontime-eventSettings', { @@ -18,3 +19,5 @@ export const defaultPublicAtom = selectAtom( eventSettingsAtom, (settings) => settings.defaultPublic ); + +export const editorEventId = atom(null); diff --git a/client/src/common/components/buttons/ActionButtons.tsx b/client/src/common/components/buttons/ActionButtons.tsx index 0e12c3689..e4e532565 100644 --- a/client/src/common/components/buttons/ActionButtons.tsx +++ b/client/src/common/components/buttons/ActionButtons.tsx @@ -1,6 +1,4 @@ -import { IconButton } from '@chakra-ui/button'; -import { Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/menu'; -import { Tooltip } from '@chakra-ui/tooltip'; +import { IconButton, Menu, MenuButton, MenuItem, MenuList, Tooltip } from '@chakra-ui/react'; import { FiClock } from '@react-icons/all-files/fi/FiClock'; import { FiMinusCircle } from '@react-icons/all-files/fi/FiMinusCircle'; import { FiPlus } from '@react-icons/all-files/fi/FiPlus'; @@ -30,10 +28,7 @@ export default function ActionButtons(props: ActionButtonProps) { aria-label='Options' size='xs' icon={} - _expanded={{ bg: 'orange.300', color: 'white' }} - _focus={{ boxShadow: 'none' }} - backgroundColor='orange.200' - color='orange.500' + colorScheme='blue' /> diff --git a/client/src/common/components/buttons/EnableBtn.jsx b/client/src/common/components/buttons/EnableBtn.jsx index 8948d5b4d..c9dace127 100644 --- a/client/src/common/components/buttons/EnableBtn.jsx +++ b/client/src/common/components/buttons/EnableBtn.jsx @@ -1,4 +1,4 @@ -import { Button } from '@chakra-ui/button'; +import { Button } from '@chakra-ui/react'; import { IoCheckmarkSharp } from '@react-icons/all-files/io5/IoCheckmarkSharp'; import { IoCloseSharp } from '@react-icons/all-files/io5/IoCloseSharp'; import PropTypes from 'prop-types'; diff --git a/client/src/common/components/buttons/PauseIconBtn.tsx b/client/src/common/components/buttons/PauseIconBtn.tsx index 1b2737a4f..5bc581a6c 100644 --- a/client/src/common/components/buttons/PauseIconBtn.tsx +++ b/client/src/common/components/buttons/PauseIconBtn.tsx @@ -1,5 +1,4 @@ -import { IconButton } from '@chakra-ui/button'; -import { Tooltip } from '@chakra-ui/tooltip'; +import { IconButton, Tooltip } from '@chakra-ui/react'; import { IoPause } from '@react-icons/all-files/io5/IoPause'; import { tooltipDelayMid } from '../../../ontimeConfig'; diff --git a/client/src/common/components/buttons/PublicIconBtn.jsx b/client/src/common/components/buttons/PublicIconBtn.jsx index 969559b85..c9f9c0374 100644 --- a/client/src/common/components/buttons/PublicIconBtn.jsx +++ b/client/src/common/components/buttons/PublicIconBtn.jsx @@ -1,5 +1,4 @@ -import { IconButton } from '@chakra-ui/button'; -import { Tooltip } from '@chakra-ui/tooltip'; +import { IconButton, Tooltip } from '@chakra-ui/react'; import { FiUsers } from '@react-icons/all-files/fi/FiUsers'; import PropTypes from 'prop-types'; diff --git a/client/src/common/components/buttons/QuitIconBtn.jsx b/client/src/common/components/buttons/QuitIconBtn.jsx index 7dd1325b2..301acaa48 100644 --- a/client/src/common/components/buttons/QuitIconBtn.jsx +++ b/client/src/common/components/buttons/QuitIconBtn.jsx @@ -1,5 +1,4 @@ import { useCallback, useContext, useEffect, useRef, useState } from 'react'; -import { Button, IconButton } from '@chakra-ui/button'; import { AlertDialog, AlertDialogBody, @@ -7,8 +6,8 @@ import { AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, -} from '@chakra-ui/modal'; -import { Tooltip } from '@chakra-ui/tooltip'; + Button, IconButton, Tooltip, +} from '@chakra-ui/react'; import { FiPower } from '@react-icons/all-files/fi/FiPower'; import PropTypes from 'prop-types'; @@ -24,7 +23,7 @@ export default function QuitIconBtn(props) { useEffect(() => { if (window.process?.type === 'renderer') { window.ipcRenderer.on('user-request-shutdown', () => { - emitInfo('Shutdown request') + emitInfo('Shutdown request'); setIsOpen(true); }); } diff --git a/client/src/common/components/buttons/RollIconBtn.jsx b/client/src/common/components/buttons/RollIconBtn.jsx index 4a0b83561..aa78ea7ba 100644 --- a/client/src/common/components/buttons/RollIconBtn.jsx +++ b/client/src/common/components/buttons/RollIconBtn.jsx @@ -1,5 +1,4 @@ -import { IconButton } from '@chakra-ui/button'; -import { Tooltip } from '@chakra-ui/tooltip'; +import { IconButton, Tooltip } from '@chakra-ui/react'; import { IoTimeOutline } from '@react-icons/all-files/io5/IoTimeOutline'; import PropTypes from 'prop-types'; @@ -25,5 +24,5 @@ export default function RollIconBtn(props) { RollIconBtn.propTypes = { clickhandler: PropTypes.func, active: PropTypes.bool, - disabled: PropTypes.bool -} + disabled: PropTypes.bool, +}; diff --git a/client/src/common/components/buttons/StartIconBtn.jsx b/client/src/common/components/buttons/StartIconBtn.jsx index 69d0de88f..73479816f 100644 --- a/client/src/common/components/buttons/StartIconBtn.jsx +++ b/client/src/common/components/buttons/StartIconBtn.jsx @@ -1,5 +1,4 @@ -import { IconButton } from '@chakra-ui/button'; -import { Tooltip } from '@chakra-ui/tooltip'; +import { IconButton, Tooltip } from '@chakra-ui/react'; import { IoPlay } from '@react-icons/all-files/io5/IoPlay'; import PropTypes from 'prop-types'; diff --git a/client/src/common/components/buttons/TooltipActionBtn.tsx b/client/src/common/components/buttons/TooltipActionBtn.tsx index e488178f8..a7da4148e 100644 --- a/client/src/common/components/buttons/TooltipActionBtn.tsx +++ b/client/src/common/components/buttons/TooltipActionBtn.tsx @@ -1,6 +1,4 @@ -import { IconButton } from '@chakra-ui/button'; -import { IconButtonProps } from '@chakra-ui/react'; -import { Tooltip } from '@chakra-ui/tooltip'; +import { IconButton, IconButtonProps, Tooltip } from '@chakra-ui/react'; interface TooltipActionBtnProps extends IconButtonProps { clickHandler: () => void; diff --git a/client/src/common/components/buttons/TooltipLoadingActionBtn.jsx b/client/src/common/components/buttons/TooltipLoadingActionBtn.jsx index 6c817ca3c..0b11953a5 100644 --- a/client/src/common/components/buttons/TooltipLoadingActionBtn.jsx +++ b/client/src/common/components/buttons/TooltipLoadingActionBtn.jsx @@ -1,6 +1,5 @@ import { useCallback, useState } from 'react'; -import { IconButton } from '@chakra-ui/button'; -import { Tooltip } from '@chakra-ui/tooltip'; +import { IconButton, Tooltip } from '@chakra-ui/react'; import PropTypes from 'prop-types'; export default function TooltipLoadingActionBtn(props) { @@ -10,7 +9,7 @@ export default function TooltipLoadingActionBtn(props) { const handleClick = useCallback(() => { setLoading(true); clickHandler(); - },[clickHandler, setLoading]); + }, [clickHandler, setLoading]); return ( diff --git a/client/src/common/components/buttons/TransportIconBtn.jsx b/client/src/common/components/buttons/TransportIconBtn.jsx index da4bb3c90..d8b07e731 100644 --- a/client/src/common/components/buttons/TransportIconBtn.jsx +++ b/client/src/common/components/buttons/TransportIconBtn.jsx @@ -1,5 +1,4 @@ -import { IconButton } from '@chakra-ui/button'; -import { Tooltip } from '@chakra-ui/tooltip'; +import { IconButton, Tooltip } from '@chakra-ui/react'; import PropTypes from 'prop-types'; import { tooltipDelayMid } from '../../../ontimeConfig'; diff --git a/client/src/common/components/buttons/UnloadIconBtn.jsx b/client/src/common/components/buttons/UnloadIconBtn.jsx index a74506668..a6e1c1f2e 100644 --- a/client/src/common/components/buttons/UnloadIconBtn.jsx +++ b/client/src/common/components/buttons/UnloadIconBtn.jsx @@ -1,5 +1,4 @@ -import { IconButton } from '@chakra-ui/button'; -import { Tooltip } from '@chakra-ui/tooltip'; +import { IconButton, Tooltip } from '@chakra-ui/react'; import { IoStop } from '@react-icons/all-files/io5/IoStop'; import PropTypes from 'prop-types'; diff --git a/client/src/common/components/errorBoundary/ErrorBoundary.jsx b/client/src/common/components/errorBoundary/ErrorBoundary.jsx index c30e73856..9f8e31c5e 100644 --- a/client/src/common/components/errorBoundary/ErrorBoundary.jsx +++ b/client/src/common/components/errorBoundary/ErrorBoundary.jsx @@ -36,7 +36,7 @@ class ErrorBoundary extends React.Component { render() { if (this.state.errorMessage) { return ( -
+

:/

Something went wrong

diff --git a/client/src/common/components/eventTimes/EventTimes.jsx b/client/src/common/components/eventTimes/EventTimes.jsx deleted file mode 100644 index dffeb68d1..000000000 --- a/client/src/common/components/eventTimes/EventTimes.jsx +++ /dev/null @@ -1,70 +0,0 @@ -import { useCallback, useContext } from 'react'; -import EditableTimer from 'common/components/input/EditableTimer'; -import PropTypes from 'prop-types'; - -import { LoggingContext } from '../../context/LoggingContext'; -import { validateTimes } from '../../utils/entryValidator'; - -export default function EventTimes(props) { - const { actionHandler, delay, timeStart, timeEnd, previousEnd } = props; - const { emitWarning } = useContext(LoggingContext); - - /** - * This code is duplicated from EventTimesVertical - * @description Validates a time input against its pair - * @param {string} entry - field to validate: timeStart, timeEnd, durationOverride - * @param {number} val - field value - * @return {boolean} - */ - const handleValidate = useCallback( - () => (entry, val) => { - if (val == null || timeStart == null || timeEnd == null) return true; - if (timeStart === 0) return true; - - let start = timeStart; - let end = timeEnd; - if (entry === 'timeStart') { - start = val; - } else if (entry === 'timeEnd') { - end = val; - } else return entry === 'durationOverride'; - - const valid = validateTimes(start, end); - // give warning but not enforce validation - if (!valid.value) { - emitWarning(`Time Input Warning: ${valid.catch}`); - } - return valid.value; - }, - [emitWarning, timeEnd, timeStart] - ); - - return ( - <> - - - - ); -} - -EventTimes.propTypes = { - actionHandler: PropTypes.func.isRequired, - delay: PropTypes.number, - timeStart: PropTypes.number, - timeEnd: PropTypes.number, - previousEnd: PropTypes.number, -}; diff --git a/client/src/common/components/eventTimes/EventTimesVertical.jsx b/client/src/common/components/eventTimes/EventTimesVertical.jsx deleted file mode 100644 index 9c5b53030..000000000 --- a/client/src/common/components/eventTimes/EventTimesVertical.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import { useCallback, useContext } from 'react'; -import PropTypes from 'prop-types'; - -import { LoggingContext } from '../../context/LoggingContext'; -import { validateTimes } from '../../utils/entryValidator'; - -import Times from './Times'; -import TimesDelayed from './TimesDelayed'; - -export default function EventTimesVertical(props) { - const { delay, timeStart, timeEnd, duration, previousEnd, actionHandler } = props; - const { emitWarning } = useContext(LoggingContext); - - /** - * This code is duplicated from EventTimes - * @description Validates a time input against its pair - * @param {string} entry - field to validate: timeStart, timeEnd, durationOverride - * @param {number} val - field value - * @return {boolean} - */ - const handleValidate = useCallback( - () => (entry, val) => { - if (val == null || timeStart == null || timeEnd == null) return true; - if (timeStart === 0) return true; - - let start = timeStart; - let end = timeEnd; - if (entry === 'timeStart') { - start = val; - } else if (entry === 'timeEnd') { - end = val; - } else return entry === 'durationOverride'; - - const valid = validateTimes(start, end); - // give warning but not enforce validation - if (!valid.value) { - emitWarning(`Time Input Warning: ${valid.catch}`); - } - return valid.value; - }, - [emitWarning, timeEnd, timeStart] - ); - - return delay != null && delay !== 0 ? ( - - ) : ( - - ); -} - -EventTimesVertical.propTypes = { - delay: PropTypes.number, - timeStart: PropTypes.number, - timeEnd: PropTypes.number, - duration: PropTypes.number, - previousEnd: PropTypes.number, - actionHandler: PropTypes.func.isRequired, -}; diff --git a/client/src/common/components/eventTimes/Times.jsx b/client/src/common/components/eventTimes/Times.jsx deleted file mode 100644 index 1edc669f9..000000000 --- a/client/src/common/components/eventTimes/Times.jsx +++ /dev/null @@ -1,50 +0,0 @@ -import PropTypes from 'prop-types'; - -import EditableTimer from '../input/EditableTimer'; - -import style from './Times.module.scss'; - -export default function Times(props) { - const { handleValidate, actionHandler, timeStart, timeEnd, duration, previousEnd } = props; - - return ( - <> - Start - - End - - Duration - - - ); -} - -Times.propTypes = { - handleValidate: PropTypes.func.isRequired, - actionHandler: PropTypes.func.isRequired, - timeStart: PropTypes.number, - timeEnd: PropTypes.number, - duration: PropTypes.number, - previousEnd: PropTypes.number, -}; diff --git a/client/src/common/components/eventTimes/Times.module.scss b/client/src/common/components/eventTimes/Times.module.scss deleted file mode 100644 index 156593f7e..000000000 --- a/client/src/common/components/eventTimes/Times.module.scss +++ /dev/null @@ -1,6 +0,0 @@ -@use '../../../theme/main' as *; - -.label { - font-size: 0.75em; - color: $label-gray; -} diff --git a/client/src/common/components/eventTimes/TimesDelayed.jsx b/client/src/common/components/eventTimes/TimesDelayed.jsx deleted file mode 100644 index c2c7fb15a..000000000 --- a/client/src/common/components/eventTimes/TimesDelayed.jsx +++ /dev/null @@ -1,59 +0,0 @@ -import PropTypes from 'prop-types'; - -import { stringFromMillis } from '../../utils/time'; -import EditableTimer from '../input/EditableTimer'; - -import style from './Times.module.scss'; - -export default function TimesDelayed(props) { - const { handleValidate, actionHandler, delay, timeStart, timeEnd, duration, previousEnd } = props; - - const scheduledStart = stringFromMillis(timeStart, false); - const scheduledEnd = stringFromMillis(timeEnd, false); - - return ( - <> - - Start {scheduledStart} - - - - End {scheduledEnd} - - - Duration - - - ); -} - -TimesDelayed.propTypes = { - handleValidate: PropTypes.func.isRequired, - actionHandler: PropTypes.func.isRequired, - delay: PropTypes.number, - timeStart: PropTypes.number, - timeEnd: PropTypes.number, - duration: PropTypes.number, - previousEnd: PropTypes.number, -}; diff --git a/client/src/common/components/input/ColourInput.module.scss b/client/src/common/components/input/ColourInput.module.scss new file mode 100644 index 000000000..e05fbf6d7 --- /dev/null +++ b/client/src/common/components/input/ColourInput.module.scss @@ -0,0 +1,10 @@ +@use '../../../theme/main' as *; + +input[type="color"] { + appearance: none; + background-color: $action-blue; + cursor: pointer; + height: 32px; + width: 32px; + padding: 0; +} diff --git a/client/src/common/components/input/ColourInput.tsx b/client/src/common/components/input/ColourInput.tsx new file mode 100644 index 000000000..5ee64e49f --- /dev/null +++ b/client/src/common/components/input/ColourInput.tsx @@ -0,0 +1,22 @@ +import { Input } from '@chakra-ui/react'; + +import style from './ColourInput.module.scss'; + +interface ColourInputProps { + value: string; + handleChange: (newValue: string) => void; +} + +export default function ColourInput(props: ColourInputProps) { + const { value, handleChange } = props; + return ( + handleChange(event.target.value)} + /> + ); +} diff --git a/client/src/common/components/input/DelayInput.jsx b/client/src/common/components/input/DelayInput.jsx index f500d775a..d46da430a 100644 --- a/client/src/common/components/input/DelayInput.jsx +++ b/client/src/common/components/input/DelayInput.jsx @@ -1,62 +1,86 @@ -import { useCallback, useEffect, useState } from 'react'; -import { Editable, EditableInput, EditablePreview } from '@chakra-ui/react'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { Input } from '@chakra-ui/react'; +import { clamp } from 'common/utils/math'; import PropTypes from 'prop-types'; -import { clamp } from '../../utils/math'; - -import style from './TimeInput.module.css'; +import style from './TimeInput.module.scss'; const inputProps = { width: 20, - fontWeight: 400, - backgroundColor: 'rgba(0,0,0,0.05)', + backgroundColor: 'rgba(255,255,255,0.13)', color: '#fff', border: '1px solid #ecc94b55', - borderRadius: '8px', + variant: 'filled', + borderRadius: '3px', placeholder: '-', textAlign: 'center', + size: 'sm', }; export default function DelayInput(props) { - const { actionHandler, value } = props; + const { submitHandler, value } = props; const [_value, setValue] = useState(value); + const inputRef = useRef(null); useEffect(() => { if (value == null) return; setValue(value); }, [value]); - const handleSubmit = useCallback( + /** + * @description Prepare delay value for update + * @param {string} value string to be parsed + */ + const validate = useCallback( (newValue) => { - if (newValue === value) return; if (newValue === '') setValue(0); + const delayValue = clamp(Number(newValue), -60, 60); - // convert to ms and updates - const msVal = clamp(newValue, -60, 60) * 60000; - actionHandler('update', { field: 'duration', value: msVal }); + if (delayValue === value) return; + setValue(delayValue); + + submitHandler(delayValue); }, - [actionHandler, value] + [submitHandler, value] ); - const labelText = `minutes ${value >= 0 ? 'delayed' : 'ahead'}`; + /** + * @description Handles common keys for submit and cancel + * @param {KeyboardEvent} event + */ + const onKeyDownHandler = useCallback((event) => { + if (event.key === 'Enter') { + inputRef.current.blur(); + validate(event.target.value); + } else if (event.key === 'Escape') { + inputRef.current.blur(); + setValue(value); + } + }, [validate, value]); + + const labelText = `${Math.abs(value) > 1 ? 'minutes' : 'minute'} ${ + value >= 0 ? 'delayed' : 'ahead' + }`; return ( -
- + setValue(v)} - onSubmit={(v) => handleSubmit(v)} - > - - - + onChange={(event) => setValue(event.target.value)} + onBlur={() => setValue(value)} + onKeyDown={onKeyDownHandler} + type='number' + /> {labelText}
); } DelayInput.propTypes = { - actionHandler: PropTypes.func, + submitHandler: PropTypes.func, value: PropTypes.number, }; diff --git a/client/src/common/components/input/EditableText.jsx b/client/src/common/components/input/EditableText.jsx deleted file mode 100644 index ffd13bf26..000000000 --- a/client/src/common/components/input/EditableText.jsx +++ /dev/null @@ -1,56 +0,0 @@ -import { useCallback, useEffect, useState } from 'react'; -import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable'; -import PropTypes from 'prop-types'; - -import style from './EditableText.module.scss'; - -export default function EditableText(props) { - const { label, defaultValue, placeholder, submitHandler, maxchar = 40, ...rest } = props; - const [text, setText] = useState(defaultValue || ''); - - useEffect(() => { - if (defaultValue == null) setText(''); - else setText(defaultValue); - }, [defaultValue]); - - const handleSubmit = useCallback((submittedVal) => { - // No need to update if it hasnt changed - if (submittedVal === defaultValue) return; - // submit a cleaned up version of the string - const cleanVal = submittedVal.trim(); - submitHandler(cleanVal); - - if (cleanVal !== submittedVal) { - setText(cleanVal); - } - },[defaultValue, submitHandler]); - - const handleChange = useCallback((val) => { - if (val.length < maxchar) setText(val); - },[maxchar]); - - return ( -
- {label} - handleChange(v)} - onSubmit={(v) => handleSubmit(v)} - value={text} - placeholder={placeholder} - className={style.inline} - {...rest} - > - - - -
- ); -} - -EditableText.propTypes = { - label: PropTypes.string, - defaultValue: PropTypes.string, - placeholder: PropTypes.string, - submitHandler: PropTypes.func.isRequired, - maxchar: PropTypes.number, -}; diff --git a/client/src/common/components/input/EditableText.module.scss b/client/src/common/components/input/EditableText.module.scss deleted file mode 100644 index f689a21e0..000000000 --- a/client/src/common/components/input/EditableText.module.scss +++ /dev/null @@ -1,28 +0,0 @@ -@use '../../../theme/main' as *; - -.block { - overflow: hidden; - display: flex; - align-items: center; - width: 100%; - - .title { - padding-left: 1em; - font-size: 0.75em; - color: $label-gray; - display: inline-block; - min-width: 6em; - } - - .preview { - color: $bg-gray-500; - } - - .inline { - display: inline; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - flex: 1; - } -} diff --git a/client/src/common/components/input/EditableTimer.jsx b/client/src/common/components/input/EditableTimer.jsx deleted file mode 100644 index 6efa7e57d..000000000 --- a/client/src/common/components/input/EditableTimer.jsx +++ /dev/null @@ -1,97 +0,0 @@ -import { useCallback, useContext, useEffect, useState } from 'react'; -import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable'; -import PropTypes from 'prop-types'; - -import { LoggingContext } from '../../context/LoggingContext'; -import { forgivingStringToMillis } from '../../utils/dateConfig'; -import { stringFromMillis } from '../../utils/time'; - -import style from './EditableTimer.module.scss'; - -export default function EditableTimer(props) { - const { name, actionHandler, time = 0, delay, validate, previousEnd } = props; - const { emitError } = useContext(LoggingContext); - const [value, setValue] = useState(''); - - - const handleSubmit = useCallback((value) => { - // Check if there is anything there - if (value === '') return false; - - let newValMillis = 0; - - // check for known aliases - if (value === 'p' || value === 'prev' || value === 'previous') { - // string to pass should be the time of the end before - if (previousEnd != null) { - newValMillis = previousEnd; - } - } else if (value.startsWith('+') || value.startsWith('p+') || value.startsWith('p +')) { - // string to pass should add to the end before - const val = value.substring(1); - newValMillis = previousEnd + forgivingStringToMillis(val); - } else { - // convert entered value to milliseconds - newValMillis = forgivingStringToMillis(value); - } - - // Time now and time submittedVal - const originalMillis = time + delay; - - // check if time is different from before - if (newValMillis === originalMillis) return false; - - // validate with parent - if (!validate(name, newValMillis)) return false; - - // update entry - actionHandler('update', { field: name, value: newValMillis }); - - return true; - },[actionHandler, delay, name, previousEnd, time, validate]); - - // prepare time fields - const validateValue = useCallback((value) => { - const success = handleSubmit(value); - if (success) { - const ms = forgivingStringToMillis(value); - setValue(stringFromMillis(ms + delay)); - } else { - setValue(stringFromMillis(time + delay)); - } - },[delay, handleSubmit, time]); - - useEffect(() => { - if (time == null) return; - try { - setValue(stringFromMillis(time + delay)); - } catch (error) { - emitError(`Unable to parse date: ${error.text}`); - } - }, [time, delay, emitError]); - - const isDelayed = delay != null && delay !== 0; - - return ( - setValue(v)} - onSubmit={(v) => validateValue(v)} - onCancel={() => setValue(stringFromMillis(time + delay, true))} - value={value} - className={isDelayed ? style.delayedEditable : style.editable} - > - - - - ); -} - -EditableTimer.propTypes = { - name: PropTypes.string.isRequired, - actionHandler: PropTypes.func.isRequired, - time: PropTypes.number, - delay: PropTypes.number, - validate: PropTypes.func.isRequired, - previousEnd: PropTypes.number, -}; diff --git a/client/src/common/components/input/EditableTimer.module.scss b/client/src/common/components/input/EditableTimer.module.scss deleted file mode 100644 index b7321c028..000000000 --- a/client/src/common/components/input/EditableTimer.module.scss +++ /dev/null @@ -1,17 +0,0 @@ -@use '../../../theme/main'as *; - -.editable, -.delayedEditable { - background-color: $input-bg; - border: $input-border; - width: 6.5em; - letter-spacing: 1px; - height: fit-content; - - text-align: center; - border-radius: 8px; -} - -.delayedEditable { - border: $input-delayed-border; -} diff --git a/client/src/common/components/input/TextInput.jsx b/client/src/common/components/input/TextInput.jsx new file mode 100644 index 000000000..7796f7311 --- /dev/null +++ b/client/src/common/components/input/TextInput.jsx @@ -0,0 +1,106 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { Input, Textarea } from '@chakra-ui/react'; +import PropTypes from 'prop-types'; + +export default function TextInput(props) { + const { isTextArea, size = 'sm', field, initialText = '', submitHandler } = props; + const inputRef = useRef(null); + const [text, setText] = useState(initialText); + + useEffect(() => { + if (typeof initialText === 'undefined') { + setText(''); + } else { + setText(initialText); + } + }, [initialText]); + + /** + * @description Handles Input value change + * @param {string} newValue + */ + const handleChange = useCallback( + (newValue) => { + if (newValue !== text) { + setText(newValue); + } + }, + [text] + ); + + /** + * @description Handles submit events + * @param {string} valueToSubmit + */ + const handleSubmit = useCallback( + (valueToSubmit) => { + // No need to update if it hasn't changed + if (valueToSubmit === initialText) { + return; + } + const cleanVal = valueToSubmit.trim(); + submitHandler(field, cleanVal); + + if (cleanVal !== valueToSubmit) { + setText(cleanVal); + } + }, + [field, initialText, submitHandler] + ); + + /** + * @description Resets input value to given + */ + const resetValue = useCallback(async () => { + setText(initialText); + },[initialText]) + + /** + * @description Handles common keys for submit and cancel + * @param {KeyboardEvent} event + */ + const keyHandler = useCallback( + (event) => { + if (event.key === 'Escape') { + resetValue(); + } else if (event.key === 'Enter') { + if (!isTextArea) { + handleSubmit(text); + } + } + }, + [resetValue, isTextArea, handleSubmit, text] + ); + + return isTextArea ? ( +