diff --git a/src/app.tsx b/src/app.tsx
index 66d23e2..58bdc2a 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -1,176 +1,34 @@
-import { useTheme } from 'next-themes';
-import { useMemo, useState } from 'preact/hooks';
-import { Logo } from './components/Logo';
-import QRModal from './components/QRModal';
-import Section from './components/Section';
-import Button, { Variant } from './components/core/Button';
-import {
- getQRCodeData,
- resetSections,
- resetToDefaultConfig,
- uploadConfig,
- useQRScoutState,
-} from './store/store';
+import { useState } from 'preact/hooks';
+import { Footer } from './components/Footer';
+import { Header } from './components/Header';
+import { QRModal } from './components/QR';
+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]);
-
- 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 (
-
-
QRScout|{formData.title}
-
-
-
+
{formData.page_title}
- setShowQR(false)}
- />
+ setShowQR(false)} />
-
+
);
}
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/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 && (
+ <>
+
+
+ >
+ )}
+ >
+ );
+}
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 52dc66e..0000000
--- a/src/components/QRModal.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import QRCode from 'qrcode.react';
-
-export interface QRModalProps {
- show: boolean;
- title: string;
- data: string;
- onDismiss: () => void;
-}
-
-export default function QRModal(props: QRModalProps) {
- return (
- <>
- {props.show && (
- <>
-
-
-
-
{props.title.toUpperCase()}
-
-
-
- navigator.clipboard.writeText(props.data + '\n')
- }
- >
-
-
-
-
-
-
- >
- )}
- >
- );
-}
diff --git a/src/components/Sections/CommitAndResetSection/CommitAndResetSection.tsx b/src/components/Sections/CommitAndResetSection/CommitAndResetSection.tsx
new file mode 100644
index 0000000..d7cd8bf
--- /dev/null
+++ b/src/components/Sections/CommitAndResetSection/CommitAndResetSection.tsx
@@ -0,0 +1,34 @@
+import { useMemo } from 'preact/hooks';
+import { useQRScoutState } from '../../../store/store';
+import { CommitButton } from './CommitButton';
+import { ResetButton } from './ResetButton';
+
+export type CommitAndResetSectionProps = {
+ onCommit: () => 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/Sections/CommitAndResetSection/CommitButton.tsx b/src/components/Sections/CommitAndResetSection/CommitButton.tsx
new file mode 100644
index 0000000..4767218
--- /dev/null
+++ b/src/components/Sections/CommitAndResetSection/CommitButton.tsx
@@ -0,0 +1,17 @@
+export type CommitButtonProps = {
+ onClick: () => void;
+ disabled: boolean;
+};
+
+export function CommitButton(props: CommitButtonProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Sections/CommitAndResetSection/ResetButton.tsx b/src/components/Sections/CommitAndResetSection/ResetButton.tsx
new file mode 100644
index 0000000..35d0f98
--- /dev/null
+++ b/src/components/Sections/CommitAndResetSection/ResetButton.tsx
@@ -0,0 +1,18 @@
+import { resetSections } from '../../../store/store';
+
+export type ResetButtonProps = {
+ disabled?: boolean;
+};
+
+export function ResetButton(props: ResetButtonProps) {
+ return (
+
+ );
+}
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 91%
rename from src/components/Section.tsx
rename to src/components/Sections/FormSection.tsx
index 50db6b3..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';
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/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 {
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 9105b91..0d2bb4e 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) {
@@ -91,11 +91,10 @@ export const inputSelector =
?.fields.find(f => f.code === code);
};
-export function getQRCodeData(): string {
+export function getFieldValue(code: string) {
return useQRScoutState
.getState()
.formData.sections.map(s => s.fields)
.flat()
- .map(v => `${v.value}`.replace(/\n/g, ' '))
- .join('\t');
+ .find(f => f.code === code)?.value;
}