diff --git a/config/2024/config.json b/config/2024/config.json index b988699..53b36cf 100644 --- a/config/2024/config.json +++ b/config/2024/config.json @@ -73,6 +73,14 @@ "required": false, "code": "Mved" }, + { + "code": "timer", + "title": "Timer", + "type": "timer", + "defaultValue": 0, + "min": 0, + "required": false + }, { "code": "ausc", "title": "Speaker Scored", diff --git a/config/schema.json b/config/schema.json index ac96bb5..9f85202 100644 --- a/config/schema.json +++ b/config/schema.json @@ -99,7 +99,8 @@ "range", "select", "counter", - "image" + "image", + "timer" ] } } diff --git a/package-lock.json b/package-lock.json index f4f0089..713c632 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "clsx": "^2.1.0", "immer": "^10.0.3", "lodash": "^4.17.21", + "lucide-react": "^0.456.0", "next-themes": "^0.2.1", "preact": "^10.19.3", "qrcode.react": "^3.1.0", @@ -2294,6 +2295,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.456.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.456.0.tgz", + "integrity": "sha512-DIIGJqTT5X05sbAsQ+OhA8OtJYyD4NsEMCA/HQW/Y6ToPQ7gwbtujIoeAaup4HpHzV35SQOarKAWH8LYglB6eA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, "node_modules/magic-string": { "version": "0.30.5", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", diff --git a/package.json b/package.json index ca3b38e..86a0a64 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "clsx": "^2.1.0", "immer": "^10.0.3", "lodash": "^4.17.21", + "lucide-react": "^0.456.0", "next-themes": "^0.2.1", "preact": "^10.19.3", "qrcode.react": "^3.1.0", diff --git a/src/components/inputs/BaseInputProps.ts b/src/components/inputs/BaseInputProps.ts index ceae405..3a7f3f4 100644 --- a/src/components/inputs/BaseInputProps.ts +++ b/src/components/inputs/BaseInputProps.ts @@ -37,4 +37,5 @@ export type InputTypes = | 'range' | 'select' | 'counter' - | 'image'; + | 'image' + | 'timer'; diff --git a/src/components/inputs/ConfigurableInput.tsx b/src/components/inputs/ConfigurableInput.tsx index 7203b46..74c4436 100644 --- a/src/components/inputs/ConfigurableInput.tsx +++ b/src/components/inputs/ConfigurableInput.tsx @@ -5,6 +5,7 @@ import NumberInput from './NumberInput'; import RangeInput from './RangeInput'; import SelectInput from './SelectInput'; import StringInput from './StringInput'; +import TimerInput from './TimerInput'; export interface ConfigurableInputProps { section: string; @@ -85,6 +86,18 @@ export default function ConfigurableInput(props: ConfigurableInputProps) { section={props.section} /> ); + case 'timer': + return ( + + ) default: return (
diff --git a/src/components/inputs/TimerInput.tsx b/src/components/inputs/TimerInput.tsx new file mode 100644 index 0000000..13a2c43 --- /dev/null +++ b/src/components/inputs/TimerInput.tsx @@ -0,0 +1,87 @@ +import { Pause, Play, TimerReset, Undo } from 'lucide-react'; +import BaseInputProps from './BaseInputProps'; +import { useState, useEffect, useMemo } from 'react'; +export interface TimerInputProps extends BaseInputProps { + min?: number; + max?: number; + step?: number; + defaultValue?: number; +} +function getAvg(array: any[]) { + if (array.length === 0) { + return 0; + } + let avg = 0; + array.forEach((num) => { + avg += num; + }); + return avg / array.length; +} +export default function TimerInput(data: TimerInputProps) { + const [time, setTime] = useState(0); + const [isRunning, toggleTimer] = useState(false); + const [times, setTimes] = useState([]); + + useEffect(() => { + if (data.value === data.defaultValue && times.length > 0) { + clearTimer(); + setTimes([]); + } + }, [data.value, data.defaultValue, times]); + + function startStop() { + toggleTimer(!isRunning); + } + + function clearTimer(update: boolean = false) { + if (update) { + updateTimes(time / 100); + } + setTime(0); + toggleTimer(false); + } + + function updateTimes(newValue: number) { + data.onChange(getAvg([...times, newValue])) + setTimes((old) => ([...old, newValue])); + } + + useEffect(() => { + let intervalId: number; + if (isRunning) { + intervalId = setInterval(() => setTime(time + (data.step || 1)), 10); + } + return () => clearInterval(intervalId); + }, [isRunning, time]); + + + return ( +
+

{`${data.value.toFixed(3)} (${times.length})`}

+

{(time / 100).toFixed(2)}

+
+ + + +
+
+ ); +}