Skip to content

Commit aeefc59

Browse files
authored
Merge pull request #76 from FRC2713/image_input
add image input
2 parents 09912cc + c22a44b commit aeefc59

File tree

5 files changed

+208
-2
lines changed

5 files changed

+208
-2
lines changed

README.md

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ The basic structure of the config.json file is as follows:
8484

8585
`title`: The name of this field
8686

87-
`type`: One of "text", "number", "boolean", "range", "select", "counter", "image", "timer", or "multiselect". Describes the type of input this is.
87+
`type`: One of "text", "number", "boolean", "range", "select", "counter", "timer", "multi-select", or "image". Describes the type of input this is.
8888

8989
`required`: a boolean indicating if this must be filled out before the QRCode is generated. If any field with this set to true is not filled out, QRScout will not generate a QRCode when the commit button is pressed.
9090

@@ -192,3 +192,72 @@ For example, in a game where robots can score in multiple locations, you might c
192192
```
193193

194194
This allows scouts to quickly record all locations where a robot successfully scored during a match.
195+
196+
### Using Image Input
197+
198+
The image input type allows you to display static images in your scouting form. This is useful for showing field layouts, robot diagrams, game piece locations, or any visual reference that helps scouts accurately record data.
199+
200+
#### Configuration in config.json
201+
202+
To configure an image field in your `config.json`:
203+
204+
```json
205+
{
206+
"title": "Field Layout",
207+
"type": "image",
208+
"required": false,
209+
"code": "fieldLayout",
210+
"description": "Reference diagram of the field",
211+
"defaultValue": "https://example.com/path/to/field-layout.jpg",
212+
"width": 400,
213+
"height": 300,
214+
"alt": "2024 FRC Field Layout Diagram",
215+
"formResetBehavior": "preserve"
216+
}
217+
```
218+
219+
#### Image Input Properties
220+
221+
- **defaultValue**: The URL to the statically hosted image. This should be a publicly accessible URL.
222+
- **width** (optional): The width of the image in pixels. If not specified, the image will use responsive sizing.
223+
- **height** (optional): The height of the image in pixels. If not specified, the image will maintain its aspect ratio.
224+
- **alt** (optional): Alternative text for the image for accessibility. If not provided, it will use the title.
225+
226+
#### Interactive Features
227+
228+
- **Click to Enlarge**: Users can click on any image to open a full-size version in a dialog. This is particularly useful for detailed diagrams or when images need to be examined more closely.
229+
230+
#### Best Practices for Image Input
231+
232+
1. **Host Images Reliably**: Ensure your images are hosted on a reliable service that will be accessible during competition, even with limited internet connectivity.
233+
2. **Optimize Image Size**: Use appropriately sized and compressed images to ensure fast loading times, especially on tablets or devices with slower connections.
234+
3. **Consider Offline Use**: For critical reference images, consider embedding them directly in your application or providing a local fallback.
235+
4. **Use Descriptive Alt Text**: Provide meaningful alternative text to ensure accessibility for all users.
236+
5. **Provide Context**: Let users know they can click on images to view them in full size, especially for detailed diagrams or maps.
237+
238+
#### FRC Scouting Examples
239+
240+
Image inputs are particularly useful for FRC scouting in scenarios like:
241+
242+
- **Field Layout Reference**: Show the competition field with labeled zones for more accurate position reporting
243+
- **Robot Diagram**: Display a diagram of a robot with numbered components for reference
244+
- **Scoring Locations**: Visualize different scoring positions or game elements
245+
- **Strategy Diagrams**: Show predefined strategies or paths that scouts should watch for
246+
- **Game Piece Identification**: Display images of the current season's game pieces for reference
247+
248+
For example, to include a field diagram in your scouting form:
249+
250+
```json
251+
{
252+
"title": "Field Reference",
253+
"type": "image",
254+
"required": false,
255+
"code": "fieldReference",
256+
"description": "Use this diagram to identify field positions",
257+
"defaultValue": "https://yourteam.org/resources/field-diagram-2024.jpg",
258+
"width": 500,
259+
"formResetBehavior": "preserve"
260+
}
261+
```
262+
263+
This allows scouts to reference the field layout while recording robot positions or movements during a match.

config/2025/config.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,18 @@
138138
"SP1": "Deep",
139139
"SP2": "Shallow"
140140
}
141+
},
142+
{
143+
"title": "Field Layout",
144+
"description": "Reference diagram of the field",
145+
"type": "image",
146+
"required": false,
147+
"code": "fieldLayout",
148+
"defaultValue": "https://firstfrc.blob.core.windows.net/frc2025/Manual/HTML/2025GameManual_files/image008.png",
149+
"width": 400,
150+
"height": 300,
151+
"alt": "2025 FRC Field Layout Diagram",
152+
"formResetBehavior": "preserve"
141153
}
142154
]
143155
},

src/components/inputs/BaseInputProps.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const inputTypeSchema = z
1010
'counter',
1111
'timer',
1212
'multi-select',
13+
'image',
1314
])
1415
.describe('The type of input');
1516

@@ -82,6 +83,17 @@ export const timerInputSchema = inputBaseSchema.extend({
8283
defaultValue: z.number().default(0).describe('The default value'),
8384
});
8485

86+
export const imageInputSchema = inputBaseSchema.extend({
87+
type: z.literal('image'),
88+
defaultValue: z
89+
.string()
90+
.default('')
91+
.describe('The URL to a statically hosted image'),
92+
width: z.number().optional().describe('The width of the image in pixels'),
93+
height: z.number().optional().describe('The height of the image in pixels'),
94+
alt: z.string().optional().describe('The alt text for the image'),
95+
});
96+
8597
export const sectionSchema = z.object({
8698
name: z.string(),
8799
fields: z.array(
@@ -94,6 +106,7 @@ export const sectionSchema = z.object({
94106
rangeInputSchema,
95107
booleanInputSchema,
96108
timerInputSchema,
109+
imageInputSchema,
97110
]),
98111
),
99112
});
@@ -105,7 +118,7 @@ const shadcnColorSchema = z
105118
const shadcnRadiusSchema = z
106119
.string()
107120
.regex(/([0-9]*.[0-9]+rem)/)
108-
.optional()
121+
.optional();
109122

110123
export const colorSchemeSchema = z.object({
111124
background: shadcnColorSchema,
@@ -227,6 +240,7 @@ export type CounterInputData = z.infer<typeof counterInputSchema>;
227240
export type RangeInputData = z.infer<typeof rangeInputSchema>;
228241
export type BooleanInputData = z.infer<typeof booleanInputSchema>;
229242
export type TimerInputData = z.infer<typeof timerInputSchema>;
243+
export type ImageInputData = z.infer<typeof imageInputSchema>;
230244

231245
export type InputPropsMap = {
232246
text: StringInputData;
@@ -237,6 +251,7 @@ export type InputPropsMap = {
237251
'multi-select': MultiSelectInputData;
238252
counter: CounterInputData;
239253
timer: TimerInputData;
254+
image: ImageInputData;
240255
};
241256

242257
export type SectionProps = z.infer<typeof sectionSchema>;

src/components/inputs/ConfigurableInput.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { InputTypes } from './BaseInputProps';
22
import CheckboxInput from './CheckboxInput';
33
import CounterInput from './CounterInput';
4+
import ImageInput from './ImageInput';
45
import NumberInput from './NumberInput';
56
import RangeInput from './RangeInput';
67
import SelectInput from './SelectInput';
@@ -17,6 +18,8 @@ export default function ConfigurableInput(props: ConfigurableInputProps) {
1718
switch (props.type) {
1819
case 'text':
1920
return <StringInput {...props} key={props.code} />;
21+
case 'image':
22+
return <ImageInput {...props} key={props.code} />;
2023
case 'select':
2124
return <SelectInput {...props} key={props.code} />;
2225
case 'number':
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { useEvent } from '@/hooks';
2+
import React, { useCallback, useEffect } from 'react';
3+
import { inputSelector, updateValue, useQRScoutState } from '../../store/store';
4+
import { Card } from '../ui/card';
5+
import { ImageInputData } from './BaseInputProps';
6+
import { ConfigurableInputProps } from './ConfigurableInput';
7+
import { cn } from '@/lib/utils';
8+
import { Dialog, DialogContent, DialogClose } from '../ui/dialog';
9+
10+
export default function ImageInput(props: ConfigurableInputProps) {
11+
const data = useQRScoutState(
12+
inputSelector<ImageInputData>(props.section, props.code),
13+
);
14+
const [isDialogOpen, setIsDialogOpen] = React.useState(false);
15+
16+
if (!data) {
17+
return <div>Invalid input</div>;
18+
}
19+
20+
const [value, setValue] = React.useState(data.defaultValue);
21+
22+
const resetState = useCallback(
23+
({ force }: { force: boolean }) => {
24+
console.log(
25+
`resetState ${data.code}`,
26+
`force: ${force}`,
27+
`behavior: ${data.formResetBehavior}`,
28+
);
29+
if (force) {
30+
setValue(data.defaultValue);
31+
return;
32+
}
33+
if (data.formResetBehavior === 'preserve') {
34+
return;
35+
}
36+
setValue(data.defaultValue);
37+
},
38+
[data.defaultValue],
39+
);
40+
41+
useEvent('resetFields', resetState);
42+
43+
useEffect(() => {
44+
updateValue(props.code, value);
45+
}, [value]);
46+
47+
if (!data) {
48+
return <div>Invalid input</div>;
49+
}
50+
51+
// If no image URL is provided, show a placeholder
52+
if (!value) {
53+
return (
54+
<Card className="flex items-center justify-center p-4 bg-muted text-muted-foreground">
55+
No image provided
56+
</Card>
57+
);
58+
}
59+
60+
return (
61+
<div className="relative">
62+
<img
63+
src={value}
64+
alt={data.alt || `${data.title} image`}
65+
className={cn(
66+
'w-full h-auto object-contain rounded-md cursor-pointer',
67+
data.disabled && 'opacity-50',
68+
)}
69+
style={{
70+
width: data.width ? `${data.width}px` : '100%',
71+
height: data.height ? `${data.height}px` : 'auto',
72+
}}
73+
onClick={() => setIsDialogOpen(true)}
74+
/>
75+
76+
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
77+
<DialogContent className="max-w-[90vw] max-h-[90vh] p-0 overflow-hidden">
78+
<div className="relative w-full h-full">
79+
<img
80+
src={value}
81+
alt={data.alt || `${data.title} image`}
82+
className="w-full h-full object-contain"
83+
/>
84+
<DialogClose className="absolute right-2 top-2 rounded-full bg-background/80 p-2 hover:bg-background/90 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2">
85+
<span className="sr-only">Close</span>
86+
<svg
87+
xmlns="http://www.w3.org/2000/svg"
88+
width="24"
89+
height="24"
90+
viewBox="0 0 24 24"
91+
fill="none"
92+
stroke="currentColor"
93+
strokeWidth="2"
94+
strokeLinecap="round"
95+
strokeLinejoin="round"
96+
className="h-4 w-4"
97+
>
98+
<line x1="18" y1="6" x2="6" y2="18"></line>
99+
<line x1="6" y1="6" x2="18" y2="18"></line>
100+
</svg>
101+
</DialogClose>
102+
</div>
103+
</DialogContent>
104+
</Dialog>
105+
</div>
106+
);
107+
}

0 commit comments

Comments
 (0)