From f9f70cb3bb2003c7b2edb91e5cdc8c879f0b88e4 Mon Sep 17 00:00:00 2001 From: Ty Tremblay Date: Fri, 23 Feb 2024 09:42:10 -0500 Subject: [PATCH 1/5] move inputs into components folder --- config/schema.json | 4 + src/app.tsx | 110 ++++++++++-------- src/components/CommitButton.tsx | 17 +++ src/components/ResetButton.tsx | 17 +++ src/components/Section.tsx | 6 +- src/{ => components}/inputs/BaseInputProps.ts | 0 src/{ => components}/inputs/CheckboxInput.tsx | 0 .../inputs/ConfigurableInput.tsx | 2 +- src/{ => components}/inputs/CounterInput.tsx | 0 src/{ => components}/inputs/InputCard.tsx | 0 src/{ => components}/inputs/NumberInput.tsx | 0 src/{ => components}/inputs/RangeInput.tsx | 0 src/{ => components}/inputs/SelectInput.tsx | 0 src/{ => components}/inputs/StringInput.tsx | 2 +- 14 files changed, 102 insertions(+), 56 deletions(-) create mode 100644 src/components/CommitButton.tsx create mode 100644 src/components/ResetButton.tsx rename src/{ => components}/inputs/BaseInputProps.ts (100%) rename src/{ => components}/inputs/CheckboxInput.tsx (100%) rename src/{ => components}/inputs/ConfigurableInput.tsx (99%) rename src/{ => components}/inputs/CounterInput.tsx (100%) rename src/{ => components}/inputs/InputCard.tsx (100%) rename src/{ => components}/inputs/NumberInput.tsx (100%) rename src/{ => components}/inputs/RangeInput.tsx (100%) rename src/{ => components}/inputs/SelectInput.tsx (100%) rename src/{ => components}/inputs/StringInput.tsx (92%) diff --git a/config/schema.json b/config/schema.json index 6b2c612..e375fe6 100644 --- a/config/schema.json +++ b/config/schema.json @@ -56,6 +56,10 @@ "required": { "type": "boolean" }, + "preserveDataOnReset": { + "type": "boolean", + "description": "If true, field data will not be cleared when the scouting form is reset." + }, "code": { "type": "string" }, diff --git a/src/app.tsx b/src/app.tsx index 66d23e2..120e3c1 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,22 +1,69 @@ import { useTheme } from 'next-themes'; import { useMemo, useState } from 'preact/hooks'; +import { CommitButton } from './components/CommitButton'; import { Logo } from './components/Logo'; import QRModal from './components/QRModal'; +import { ResetButton } from './components/ResetButton'; import Section from './components/Section'; import Button, { Variant } from './components/core/Button'; +import { Config } from './components/inputs/BaseInputProps'; import { getQRCodeData, - resetSections, resetToDefaultConfig, uploadConfig, useQRScoutState, } from './store/store'; +/** + * Get the value of a field from the form data + * @param code The code of the field to get the value of + * @param formData The form data to get the value from + * @returns The value of the field + */ +function getFieldValue(code: string, formData: Config): any { + return formData.sections + .map(s => s.fields) + .flat() + .find(f => f.code === code)?.value; +} + +/** + * Download a text file + * @param filename The name of the file + * @param text The text to put in the file + */ +function download(filename: string, text: string) { + var element = document.createElement('a'); + element.setAttribute( + 'href', + 'data:text/plain;charset=utf-8,' + encodeURIComponent(text), + ); + element.setAttribute('download', filename); + + element.style.display = 'none'; + document.body.appendChild(element); + + element.click(); + + document.body.removeChild(element); +} + +/** + * Download the current form data as a json file + * @param formData The form data to download + */ +function downloadConfig(formData: Config) { + const configDownload = { ...formData }; + + configDownload.sections.forEach(s => + s.fields.forEach(f => (f.value = undefined)), + ); + download('QRScout_config.json', JSON.stringify(configDownload)); +} + export function App() { const { theme, setTheme } = useTheme(); - const formData = useQRScoutState(state => state.formData); - const [showQR, setShowQR] = useState(false); const missingRequiredFields = useMemo(() => { @@ -30,38 +77,6 @@ export function App() { ); }, [formData]); - function getFieldValue(code: string): any { - return formData.sections - .map(s => s.fields) - .flat() - .find(f => f.code === code)?.value; - } - - function download(filename: string, text: string) { - var element = document.createElement('a'); - element.setAttribute( - 'href', - 'data:text/plain;charset=utf-8,' + encodeURIComponent(text), - ); - element.setAttribute('download', filename); - - element.style.display = 'none'; - document.body.appendChild(element); - - element.click(); - - document.body.removeChild(element); - } - - function downloadConfig() { - const configDownload = { ...formData }; - - configDownload.sections.forEach(s => - s.fields.forEach(f => (f.value = undefined)), - ); - download('QRScout_config.json', JSON.stringify(configDownload)); - } - return (
@@ -75,7 +90,10 @@ export function App() { setShowQR(false)} /> @@ -87,21 +105,11 @@ export function App() { })}
- - + onClick={() => setShowQR(true)} + /> +
diff --git a/src/components/CommitButton.tsx b/src/components/CommitButton.tsx new file mode 100644 index 0000000..4767218 --- /dev/null +++ b/src/components/CommitButton.tsx @@ -0,0 +1,17 @@ +export type CommitButtonProps = { + onClick: () => void; + disabled: boolean; +}; + +export function CommitButton(props: CommitButtonProps) { + return ( + + ); +} diff --git a/src/components/ResetButton.tsx b/src/components/ResetButton.tsx new file mode 100644 index 0000000..3a5a35d --- /dev/null +++ b/src/components/ResetButton.tsx @@ -0,0 +1,17 @@ +import { resetSections } from '../store/store'; + +export type ResetButtonProps = { + disabled?: boolean; +}; + +export function ResetButton(props: ResetButtonProps) { + return ( + + ); +} diff --git a/src/components/Section.tsx b/src/components/Section.tsx index 50db6b3..f7db2f1 100644 --- a/src/components/Section.tsx +++ b/src/components/Section.tsx @@ -1,7 +1,7 @@ -import { InputProps } from '../inputs/BaseInputProps'; -import ConfigurableInput from '../inputs/ConfigurableInput'; -import InputCard from '../inputs/InputCard'; import { useQRScoutState } from '../store/store'; +import { InputProps } from './inputs/BaseInputProps'; +import ConfigurableInput from './inputs/ConfigurableInput'; +import InputCard from './inputs/InputCard'; interface SectionProps { name: string; diff --git a/src/inputs/BaseInputProps.ts b/src/components/inputs/BaseInputProps.ts similarity index 100% rename from src/inputs/BaseInputProps.ts rename to src/components/inputs/BaseInputProps.ts diff --git a/src/inputs/CheckboxInput.tsx b/src/components/inputs/CheckboxInput.tsx similarity index 100% rename from src/inputs/CheckboxInput.tsx rename to src/components/inputs/CheckboxInput.tsx diff --git a/src/inputs/ConfigurableInput.tsx b/src/components/inputs/ConfigurableInput.tsx similarity index 99% rename from src/inputs/ConfigurableInput.tsx rename to src/components/inputs/ConfigurableInput.tsx index e0dbc70..dc31411 100644 --- a/src/inputs/ConfigurableInput.tsx +++ b/src/components/inputs/ConfigurableInput.tsx @@ -1,4 +1,4 @@ -import { inputSelector, updateValue, useQRScoutState } from '../store/store'; +import { inputSelector, updateValue, useQRScoutState } from '../../store/store'; import Checkbox from './CheckboxInput'; import CounterInput from './CounterInput'; import NumberInput from './NumberInput'; diff --git a/src/inputs/CounterInput.tsx b/src/components/inputs/CounterInput.tsx similarity index 100% rename from src/inputs/CounterInput.tsx rename to src/components/inputs/CounterInput.tsx diff --git a/src/inputs/InputCard.tsx b/src/components/inputs/InputCard.tsx similarity index 100% rename from src/inputs/InputCard.tsx rename to src/components/inputs/InputCard.tsx diff --git a/src/inputs/NumberInput.tsx b/src/components/inputs/NumberInput.tsx similarity index 100% rename from src/inputs/NumberInput.tsx rename to src/components/inputs/NumberInput.tsx diff --git a/src/inputs/RangeInput.tsx b/src/components/inputs/RangeInput.tsx similarity index 100% rename from src/inputs/RangeInput.tsx rename to src/components/inputs/RangeInput.tsx diff --git a/src/inputs/SelectInput.tsx b/src/components/inputs/SelectInput.tsx similarity index 100% rename from src/inputs/SelectInput.tsx rename to src/components/inputs/SelectInput.tsx diff --git a/src/inputs/StringInput.tsx b/src/components/inputs/StringInput.tsx similarity index 92% rename from src/inputs/StringInput.tsx rename to src/components/inputs/StringInput.tsx index 55be539..aefffa8 100644 --- a/src/inputs/StringInput.tsx +++ b/src/components/inputs/StringInput.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { inputSelector, useQRScoutState } from '../store/store'; +import { inputSelector, useQRScoutState } from '../../store/store'; import BaseInputProps from './BaseInputProps'; export interface StringInputProps extends BaseInputProps { From fcc5e1e42e22715fdfa7f3d6a7250c25404b8373 Mon Sep 17 00:00:00 2001 From: Ty Tremblay Date: Fri, 23 Feb 2024 09:44:07 -0500 Subject: [PATCH 2/5] fix import path in store --- src/store/store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/store/store.ts b/src/store/store.ts index 9105b91..aeb88ff 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -1,7 +1,7 @@ import { produce } from 'immer'; import { ChangeEvent } from 'react'; import configJson from '../../config/2024/config.json'; -import { Config } from '../inputs/BaseInputProps'; +import { Config } from '../components/inputs/BaseInputProps'; import { createStore } from './createStore'; function buildConfig(c: Config) { From 4371ea342bced6a4af83e07d46dc316a0e47e5ea Mon Sep 17 00:00:00 2001 From: Ty Tremblay Date: Fri, 23 Feb 2024 10:11:01 -0500 Subject: [PATCH 3/5] more reorganizing --- src/app.tsx | 176 ++---------------- src/components/Footer.tsx | 11 ++ src/components/Header.tsx | 11 ++ src/components/QRModal.tsx | 12 +- .../CommitAndResetSection.tsx | 34 ++++ .../CommitAndResetSection}/CommitButton.tsx | 0 .../CommitAndResetSection}/ResetButton.tsx | 3 +- .../Sections/CommitAndResetSection/index.ts | 1 + .../Sections/ConfigSection/ConfigSection.tsx | 84 +++++++++ .../Sections/ConfigSection/ThemeSelector.tsx | 27 +++ .../Sections/ConfigSection/index.ts | 1 + .../{Section.tsx => Sections/FormSection.tsx} | 10 +- src/components/Sections/Sections.tsx | 13 ++ src/components/Sections/index.ts | 1 + src/store/store.ts | 8 + 15 files changed, 218 insertions(+), 174 deletions(-) create mode 100644 src/components/Footer.tsx create mode 100644 src/components/Header.tsx create mode 100644 src/components/Sections/CommitAndResetSection/CommitAndResetSection.tsx rename src/components/{ => Sections/CommitAndResetSection}/CommitButton.tsx (100%) rename src/components/{ => Sections/CommitAndResetSection}/ResetButton.tsx (83%) create mode 100644 src/components/Sections/CommitAndResetSection/index.ts create mode 100644 src/components/Sections/ConfigSection/ConfigSection.tsx create mode 100644 src/components/Sections/ConfigSection/ThemeSelector.tsx create mode 100644 src/components/Sections/ConfigSection/index.ts rename src/components/{Section.tsx => Sections/FormSection.tsx} (78%) create mode 100644 src/components/Sections/Sections.tsx create mode 100644 src/components/Sections/index.ts diff --git a/src/app.tsx b/src/app.tsx index 120e3c1..0c1e496 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,184 +1,34 @@ -import { useTheme } from 'next-themes'; -import { useMemo, useState } from 'preact/hooks'; -import { CommitButton } from './components/CommitButton'; -import { Logo } from './components/Logo'; +import { useState } from 'preact/hooks'; +import { Footer } from './components/Footer'; +import { Header } from './components/Header'; import QRModal from './components/QRModal'; -import { ResetButton } from './components/ResetButton'; -import Section from './components/Section'; -import Button, { Variant } from './components/core/Button'; -import { Config } from './components/inputs/BaseInputProps'; -import { - getQRCodeData, - resetToDefaultConfig, - uploadConfig, - useQRScoutState, -} from './store/store'; - -/** - * Get the value of a field from the form data - * @param code The code of the field to get the value of - * @param formData The form data to get the value from - * @returns The value of the field - */ -function getFieldValue(code: string, formData: Config): any { - return formData.sections - .map(s => s.fields) - .flat() - .find(f => f.code === code)?.value; -} - -/** - * Download a text file - * @param filename The name of the file - * @param text The text to put in the file - */ -function download(filename: string, text: string) { - var element = document.createElement('a'); - element.setAttribute( - 'href', - 'data:text/plain;charset=utf-8,' + encodeURIComponent(text), - ); - element.setAttribute('download', filename); - - element.style.display = 'none'; - document.body.appendChild(element); - - element.click(); - - document.body.removeChild(element); -} - -/** - * Download the current form data as a json file - * @param formData The form data to download - */ -function downloadConfig(formData: Config) { - const configDownload = { ...formData }; - - configDownload.sections.forEach(s => - s.fields.forEach(f => (f.value = undefined)), - ); - download('QRScout_config.json', JSON.stringify(configDownload)); -} +import { Sections } from './components/Sections'; +import { CommitAndResetSection } from './components/Sections/CommitAndResetSection/CommitAndResetSection'; +import { ConfigSection } from './components/Sections/ConfigSection'; +import { useQRScoutState } from './store/store'; export function App() { - const { theme, setTheme } = useTheme(); const formData = useQRScoutState(state => state.formData); const [showQR, setShowQR] = useState(false); - const missingRequiredFields = useMemo(() => { - return formData.sections - .map(s => s.fields) - .flat() - .filter( - f => - f.required && - (f.value === null || f.value === undefined || f.value === ``), - ); - }, [formData]); - return (
- - QRScout|{formData.title} - - - +

{formData.page_title}

- setShowQR(false)} - /> + setShowQR(false)} />
- {formData.sections.map(section => { - return
; - })} - -
- 0} - onClick={() => setShowQR(true)} - /> - -
-
- - - -
-
- Theme -
- -
- - -
+ + setShowQR(true)} /> +
-
-
- -
-
+
); } diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx new file mode 100644 index 0000000..a26ffe1 --- /dev/null +++ b/src/components/Footer.tsx @@ -0,0 +1,11 @@ +import { Logo } from './Logo'; + +export function Footer() { + return ( +
+
+ +
+
+ ); +} diff --git a/src/components/Header.tsx b/src/components/Header.tsx new file mode 100644 index 0000000..ce68501 --- /dev/null +++ b/src/components/Header.tsx @@ -0,0 +1,11 @@ +import { useQRScoutState } from '../store/store'; + +export function Header() { + const title = useQRScoutState(state => state.formData.title); + return ( + + QRScout|{title} + + + ); +} diff --git a/src/components/QRModal.tsx b/src/components/QRModal.tsx index 52dc66e..9b3586b 100644 --- a/src/components/QRModal.tsx +++ b/src/components/QRModal.tsx @@ -1,13 +1,15 @@ import QRCode from 'qrcode.react'; +import { getFieldValue, getQRCodeData } from '../store/store'; export interface QRModalProps { show: boolean; - title: string; - data: string; onDismiss: () => void; } export default function QRModal(props: QRModalProps) { + const title = `${getFieldValue('robot')} - ${getFieldValue( + 'matchNumber', + )}`.toUpperCase(); return ( <> {props.show && ( @@ -18,12 +20,12 @@ export default function QRModal(props: QRModalProps) { />
-

{props.title.toUpperCase()}

- +

{title}

+
- navigator.clipboard.writeText(props.data + '\n') + navigator.clipboard.writeText(getQRCodeData() + '\n') } > void; +}; + +export function CommitAndResetSection({ + onCommit, +}: CommitAndResetSectionProps) { + const formData = useQRScoutState(state => state.formData); + const missingRequiredFields = useMemo(() => { + return formData.sections + .map(s => s.fields) + .flat() + .filter( + f => + f.required && + (f.value === null || f.value === undefined || f.value === ``), + ); + }, [formData]); + + return ( +
+ 0} + onClick={onCommit} + /> + +
+ ); +} diff --git a/src/components/CommitButton.tsx b/src/components/Sections/CommitAndResetSection/CommitButton.tsx similarity index 100% rename from src/components/CommitButton.tsx rename to src/components/Sections/CommitAndResetSection/CommitButton.tsx diff --git a/src/components/ResetButton.tsx b/src/components/Sections/CommitAndResetSection/ResetButton.tsx similarity index 83% rename from src/components/ResetButton.tsx rename to src/components/Sections/CommitAndResetSection/ResetButton.tsx index 3a5a35d..35d0f98 100644 --- a/src/components/ResetButton.tsx +++ b/src/components/Sections/CommitAndResetSection/ResetButton.tsx @@ -1,4 +1,4 @@ -import { resetSections } from '../store/store'; +import { resetSections } from '../../../store/store'; export type ResetButtonProps = { disabled?: boolean; @@ -10,6 +10,7 @@ export function ResetButton(props: ResetButtonProps) { className="focus:shadow-outline mx-2 my-6 rounded bg-white py-2 font-bold uppercase text-red-rhr hover:bg-red-200 focus:outline-none dark:bg-gray-500 dark:text-white dark:hover:bg-gray-700" type="button" onClick={() => resetSections()} + disabled={props.disabled} > Reset Form diff --git a/src/components/Sections/CommitAndResetSection/index.ts b/src/components/Sections/CommitAndResetSection/index.ts new file mode 100644 index 0000000..7f3485a --- /dev/null +++ b/src/components/Sections/CommitAndResetSection/index.ts @@ -0,0 +1 @@ +export * from './CommitAndResetSection'; diff --git a/src/components/Sections/ConfigSection/ConfigSection.tsx b/src/components/Sections/ConfigSection/ConfigSection.tsx new file mode 100644 index 0000000..506f65f --- /dev/null +++ b/src/components/Sections/ConfigSection/ConfigSection.tsx @@ -0,0 +1,84 @@ +import { + resetToDefaultConfig, + uploadConfig, + useQRScoutState, +} from '../../../store/store'; +import Button, { Variant } from '../../core/Button'; +import { Config } from '../../inputs/BaseInputProps'; +import { ThemeSelector } from './ThemeSelector'; + +/** + * Download a text file + * @param filename The name of the file + * @param text The text to put in the file + */ +function download(filename: string, text: string) { + var element = document.createElement('a'); + element.setAttribute( + 'href', + 'data:text/plain;charset=utf-8,' + encodeURIComponent(text), + ); + element.setAttribute('download', filename); + + element.style.display = 'none'; + document.body.appendChild(element); + + element.click(); + + document.body.removeChild(element); +} + +/** + * Download the current form data as a json file + * @param formData The form data to download + */ +function downloadConfig(formData: Config) { + const configDownload = { ...formData }; + + configDownload.sections.forEach(s => + s.fields.forEach(f => (f.value = undefined)), + ); + download('QRScout_config.json', JSON.stringify(configDownload)); +} + +export function ConfigSection() { + const formData = useQRScoutState(state => state.formData); + return ( +
+ + + + + + +
+ ); +} diff --git a/src/components/Sections/ConfigSection/ThemeSelector.tsx b/src/components/Sections/ConfigSection/ThemeSelector.tsx new file mode 100644 index 0000000..ea47266 --- /dev/null +++ b/src/components/Sections/ConfigSection/ThemeSelector.tsx @@ -0,0 +1,27 @@ +import { useTheme } from 'next-themes'; + +export function ThemeSelector() { + const { theme, setTheme } = useTheme(); + return ( +
+
Theme
+ +
+ ); +} diff --git a/src/components/Sections/ConfigSection/index.ts b/src/components/Sections/ConfigSection/index.ts new file mode 100644 index 0000000..f30819e --- /dev/null +++ b/src/components/Sections/ConfigSection/index.ts @@ -0,0 +1 @@ +export * from './ConfigSection'; diff --git a/src/components/Section.tsx b/src/components/Sections/FormSection.tsx similarity index 78% rename from src/components/Section.tsx rename to src/components/Sections/FormSection.tsx index f7db2f1..e8bc634 100644 --- a/src/components/Section.tsx +++ b/src/components/Sections/FormSection.tsx @@ -1,13 +1,13 @@ -import { useQRScoutState } from '../store/store'; -import { InputProps } from './inputs/BaseInputProps'; -import ConfigurableInput from './inputs/ConfigurableInput'; -import InputCard from './inputs/InputCard'; +import { useQRScoutState } from '../../store/store'; +import { InputProps } from '../inputs/BaseInputProps'; +import ConfigurableInput from '../inputs/ConfigurableInput'; +import InputCard from '../inputs/InputCard'; interface SectionProps { name: string; } -export default function Section(props: SectionProps) { +export default function FormSection(props: SectionProps) { const formData = useQRScoutState(state => state.formData); const inputs = formData.sections.find(s => s.name === props.name)?.fields; return ( diff --git a/src/components/Sections/Sections.tsx b/src/components/Sections/Sections.tsx new file mode 100644 index 0000000..64a07c1 --- /dev/null +++ b/src/components/Sections/Sections.tsx @@ -0,0 +1,13 @@ +import { useQRScoutState } from '../../store/store'; +import FormSection from './FormSection'; + +export function Sections() { + const formData = useQRScoutState(state => state.formData); + return ( + <> + {formData.sections.map(section => { + return ; + })} + + ); +} diff --git a/src/components/Sections/index.ts b/src/components/Sections/index.ts new file mode 100644 index 0000000..631ad69 --- /dev/null +++ b/src/components/Sections/index.ts @@ -0,0 +1 @@ +export * from './Sections'; diff --git a/src/store/store.ts b/src/store/store.ts index aeb88ff..66993be 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -99,3 +99,11 @@ export function getQRCodeData(): string { .map(v => `${v.value}`.replace(/\n/g, ' ')) .join('\t'); } + +export function getFieldValue(code: string) { + return useQRScoutState + .getState() + .formData.sections.map(s => s.fields) + .flat() + .find(f => f.code === code)?.value; +} From 990b02e5ac8b5159d12441b5b9356bfd4c854a29 Mon Sep 17 00:00:00 2001 From: Ty Tremblay Date: Fri, 23 Feb 2024 12:03:02 -0500 Subject: [PATCH 4/5] clean up QR Code --- src/app.tsx | 2 +- src/components/QR/CloseButton.tsx | 28 ++++++++++++++ src/components/QR/CopyButton.tsx | 26 +++++++++++++ src/components/QR/PreviewText.tsx | 27 ++++++++++++++ src/components/QR/QRModal.tsx | 56 ++++++++++++++++++++++++++++ src/components/QR/index.ts | 1 + src/components/QRModal.tsx | 61 ------------------------------- src/hooks/index.ts | 1 + src/hooks/useOnClickOutside.tsx | 35 ++++++++++++++++++ src/store/store.ts | 9 ----- 10 files changed, 175 insertions(+), 71 deletions(-) create mode 100644 src/components/QR/CloseButton.tsx create mode 100644 src/components/QR/CopyButton.tsx create mode 100644 src/components/QR/PreviewText.tsx create mode 100644 src/components/QR/QRModal.tsx create mode 100644 src/components/QR/index.ts delete mode 100644 src/components/QRModal.tsx create mode 100644 src/hooks/index.ts create mode 100644 src/hooks/useOnClickOutside.tsx diff --git a/src/app.tsx b/src/app.tsx index 0c1e496..58bdc2a 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,7 +1,7 @@ import { useState } from 'preact/hooks'; import { Footer } from './components/Footer'; import { Header } from './components/Header'; -import QRModal from './components/QRModal'; +import { QRModal } from './components/QR'; import { Sections } from './components/Sections'; import { CommitAndResetSection } from './components/Sections/CommitAndResetSection/CommitAndResetSection'; import { ConfigSection } from './components/Sections/ConfigSection'; diff --git a/src/components/QR/CloseButton.tsx b/src/components/QR/CloseButton.tsx new file mode 100644 index 0000000..7efe6b7 --- /dev/null +++ b/src/components/QR/CloseButton.tsx @@ -0,0 +1,28 @@ +export type CloseButtonProps = { + onClick: () => void; +}; + +export function CloseButton(props: CloseButtonProps) { + return ( + + ); +} diff --git a/src/components/QR/CopyButton.tsx b/src/components/QR/CopyButton.tsx new file mode 100644 index 0000000..3d480af --- /dev/null +++ b/src/components/QR/CopyButton.tsx @@ -0,0 +1,26 @@ +export type CopyButtonProps = { + onCopy: () => void; + className?: string; +}; + +export function CopyButton(props: CopyButtonProps) { + return ( +
+ + + + + +
+ ); +} diff --git a/src/components/QR/PreviewText.tsx b/src/components/QR/PreviewText.tsx new file mode 100644 index 0000000..c20897f --- /dev/null +++ b/src/components/QR/PreviewText.tsx @@ -0,0 +1,27 @@ +import { CopyButton } from './CopyButton'; + +export type PreviewTextProps = { + data: string; +}; +export function PreviewText(props: PreviewTextProps) { + const chunks = props.data.split('\t'); + return ( +
+
+

+ {chunks.map((c, i) => ( + <> + {c} + + + {i !== chunks.length - 1 ? '|' : ' ↵'} + + + ))} +

+
+ + navigator.clipboard.writeText(props.data)} /> +
+ ); +} diff --git a/src/components/QR/QRModal.tsx b/src/components/QR/QRModal.tsx new file mode 100644 index 0000000..156fbdb --- /dev/null +++ b/src/components/QR/QRModal.tsx @@ -0,0 +1,56 @@ +import { useMemo, useRef } from 'preact/hooks'; +import QRCode from 'qrcode.react'; +import { useOnClickOutside } from '../../hooks/useOnClickOutside'; +import { getFieldValue, useQRScoutState } from '../../store/store'; +import { Config } from '../inputs/BaseInputProps'; +import { CloseButton } from './CloseButton'; +import { PreviewText } from './PreviewText'; + +export interface QRModalProps { + show: boolean; + onDismiss: () => void; +} + +export function getQRCodeData(formData: Config): string { + return formData.sections + .map(s => s.fields) + .flat() + .map(v => `${v.value}`.replace(/\n/g, ' ')) + .join('\t'); +} + +export function QRModal(props: QRModalProps) { + const modalRef = useRef(null); + const formData = useQRScoutState(state => state.formData); + useOnClickOutside(modalRef, props.onDismiss); + + const title = `${getFieldValue('robot')} - M${getFieldValue( + 'matchNumber', + )}`.toUpperCase(); + + const qrCodeData = useMemo(() => getQRCodeData(formData), [formData]); + return ( + <> + {props.show && ( + <> +
+
+
+

{title}

+ + +
+ +
+
+ + )} + + ); +} diff --git a/src/components/QR/index.ts b/src/components/QR/index.ts new file mode 100644 index 0000000..bfdaa03 --- /dev/null +++ b/src/components/QR/index.ts @@ -0,0 +1 @@ +export * from './QRModal'; diff --git a/src/components/QRModal.tsx b/src/components/QRModal.tsx deleted file mode 100644 index 9b3586b..0000000 --- a/src/components/QRModal.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import QRCode from 'qrcode.react'; -import { getFieldValue, getQRCodeData } from '../store/store'; - -export interface QRModalProps { - show: boolean; - onDismiss: () => void; -} - -export default function QRModal(props: QRModalProps) { - const title = `${getFieldValue('robot')} - ${getFieldValue( - 'matchNumber', - )}`.toUpperCase(); - return ( - <> - {props.show && ( - <> -
-
-
-

{title}

- -
-
- navigator.clipboard.writeText(getQRCodeData() + '\n') - } - > - - {' '} - {' '} - - -
- -
-
-
- - )} - - ); -} diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 0000000..84bc075 --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1 @@ +export * from './useOnClickOutside'; diff --git a/src/hooks/useOnClickOutside.tsx b/src/hooks/useOnClickOutside.tsx new file mode 100644 index 0000000..e6f99e9 --- /dev/null +++ b/src/hooks/useOnClickOutside.tsx @@ -0,0 +1,35 @@ +import React, { useEffect, useRef } from 'react'; + +export const useOnClickOutside = ( + ref: React.MutableRefObject, + callback: VoidFunction, +): void => { + const savedCallback = useRef(); + + useEffect(() => { + savedCallback.current = callback; + }, [callback]); + + useEffect(() => { + const listener = (event: MouseEvent | TouchEvent): void => { + if ( + ref && + ref.current && + event.target && + !ref.current.contains(event.target as Node) + ) { + if (savedCallback.current) { + savedCallback.current(); + } + } + }; + + document.addEventListener('mousedown', listener); + document.addEventListener('touchstart', listener); + + return () => { + document.removeEventListener('mousedown', listener); + document.removeEventListener('touchstart', listener); + }; + }, [ref]); +}; diff --git a/src/store/store.ts b/src/store/store.ts index 66993be..0d2bb4e 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -91,15 +91,6 @@ export const inputSelector = ?.fields.find(f => f.code === code); }; -export function getQRCodeData(): string { - return useQRScoutState - .getState() - .formData.sections.map(s => s.fields) - .flat() - .map(v => `${v.value}`.replace(/\n/g, ' ')) - .join('\t'); -} - export function getFieldValue(code: string) { return useQRScoutState .getState() From e472384a1c413df086d57a73767a781ee66add1c Mon Sep 17 00:00:00 2001 From: Ty Tremblay Date: Fri, 23 Feb 2024 12:05:53 -0500 Subject: [PATCH 5/5] revert schema --- config/schema.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/config/schema.json b/config/schema.json index e375fe6..6b2c612 100644 --- a/config/schema.json +++ b/config/schema.json @@ -56,10 +56,6 @@ "required": { "type": "boolean" }, - "preserveDataOnReset": { - "type": "boolean", - "description": "If true, field data will not be cleared when the scouting form is reset." - }, "code": { "type": "string" },