Skip to content

Commit 8040fd3

Browse files
authored
Merge pull request #241 from twilio-labs/IssuesElmerFound
Issues elmer found
2 parents 7e81078 + e8e20a1 commit 8040fd3

1 file changed

Lines changed: 87 additions & 23 deletions

File tree

  • src/app/(master-layout)/event/[slug]

src/app/(master-layout)/event/[slug]/page.tsx

Lines changed: 87 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,20 @@ function EventPage({ params }: { params: Promise<{ slug: string }> }) {
9797
function toggleSection(section: keyof typeof collapsed) {
9898
if (!isNewEvent) setCollapsed((c) => ({ ...c, [section]: !c[section] }));
9999
}
100+
101+
// Fields the user has interacted with at least once — gates the inline
102+
// per-field error messages so a blank new-event form doesn't light up red
103+
// before anyone's typed anything.
104+
const [touched, setTouched] = useState<Record<keyof EventFieldErrors, boolean>>({
105+
name: false,
106+
maxOrders: false,
107+
pickupLocation: false,
108+
senders: false,
109+
items: false,
110+
});
111+
function markTouched(field: keyof EventFieldErrors) {
112+
setTouched((t) => (t[field] ? t : { ...t, [field]: true }));
113+
}
100114
function updateMenuItemField(
101115
index: number,
102116
field: "title" | "shortTitle" | "description",
@@ -209,6 +223,10 @@ function EventPage({ params }: { params: Promise<{ slug: string }> }) {
209223
}),
210224
);
211225

226+
// Only relevant during creation — existing events have no equivalent
227+
// save-gate, so there's nothing for these to guard there.
228+
const fieldErrors = isNewEvent ? getFieldErrors(internalEvent) : {};
229+
212230
return (
213231
<div className="w-full py-8 space-y-4">
214232
{/* Page header */}
@@ -258,11 +276,17 @@ function EventPage({ params }: { params: Promise<{ slug: string }> }) {
258276
disabled={!isNewEvent}
259277
required
260278
pattern=".{4,}"
279+
aria-invalid={touched.name && !!fieldErrors.name}
280+
className={touched.name && fieldErrors.name ? "border-red-500 focus-visible:ring-red-500" : ""}
261281
value={internalEvent.name}
262282
onChange={(ev) =>
263283
updateEvent({ ...internalEvent, name: ev.target.value, slug: toKebabCase(ev.target.value) })
264284
}
285+
onBlur={() => markTouched("name")}
265286
/>
287+
{touched.name && fieldErrors.name && (
288+
<p className="text-xs text-red-500">{fieldErrors.name}</p>
289+
)}
266290
</div>
267291
<div className="space-y-1.5">
268292
<Label htmlFor="eventSlug">Slug</Label>
@@ -275,11 +299,21 @@ function EventPage({ params }: { params: Promise<{ slug: string }> }) {
275299
type="number"
276300
min={1}
277301
required
302+
aria-invalid={touched.maxOrders && !!fieldErrors.maxOrders}
303+
className={touched.maxOrders && fieldErrors.maxOrders ? "border-red-500 focus-visible:ring-red-500" : ""}
278304
value={internalEvent.maxOrders}
279305
onChange={(ev) =>
280306
updateEvent({ ...internalEvent, maxOrders: parseInt(ev.target.value) })
281307
}
308+
onBlur={() => markTouched("maxOrders")}
282309
/>
310+
{touched.maxOrders && fieldErrors.maxOrders ? (
311+
<p className="text-xs text-red-500">{fieldErrors.maxOrders}</p>
312+
) : (
313+
<p className="text-xs text-gray-400">
314+
50 or more is treated as unlimited — order confirmations won&apos;t mention a daily cap.
315+
</p>
316+
)}
283317
</div>
284318
<div className="space-y-1.5">
285319
<Label htmlFor="leadCollection">Lead Collection</Label>
@@ -330,11 +364,15 @@ function EventPage({ params }: { params: Promise<{ slug: string }> }) {
330364
<MultiSelect
331365
placeholder="Senders to be used for this event"
332366
value={internalEvent.senders.map((s) => ({ label: s, value: s }))}
333-
onChange={(selected) =>
334-
updateEvent({ ...internalEvent, senders: selected.map((s) => s.value) })
335-
}
367+
onChange={(selected) => {
368+
markTouched("senders");
369+
updateEvent({ ...internalEvent, senders: selected.map((s) => s.value) });
370+
}}
336371
options={options}
337372
/>
373+
{touched.senders && fieldErrors.senders && (
374+
<p className="text-xs text-red-500">{fieldErrors.senders}</p>
375+
)}
338376
{internalEvent.senders.length > 0 && (
339377
<Popover>
340378
<PopoverTrigger>
@@ -355,11 +393,17 @@ function EventPage({ params }: { params: Promise<{ slug: string }> }) {
355393
placeholder="Where to find the booth"
356394
required
357395
pattern=".{3,}"
396+
aria-invalid={touched.pickupLocation && !!fieldErrors.pickupLocation}
397+
className={touched.pickupLocation && fieldErrors.pickupLocation ? "border-red-500 focus-visible:ring-red-500" : ""}
358398
value={internalEvent.pickupLocation}
359399
onChange={(ev) =>
360400
updateEvent({ ...internalEvent, pickupLocation: ev.target.value })
361401
}
402+
onBlur={() => markTouched("pickupLocation")}
362403
/>
404+
{touched.pickupLocation && fieldErrors.pickupLocation && (
405+
<p className="text-xs text-red-500">{fieldErrors.pickupLocation}</p>
406+
)}
363407
</div>
364408
<div className="space-y-1.5">
365409
<Label htmlFor="language">Language</Label>
@@ -412,6 +456,7 @@ function EventPage({ params }: { params: Promise<{ slug: string }> }) {
412456
menus={config.menus}
413457
selection={internalEvent.selection}
414458
onSelectionChange={(newSelection) => {
459+
markTouched("items");
415460
updateEvent({ ...internalEvent, selection: newSelection });
416461
if (!isNewEvent) {
417462
// Debounce the save (like updateMenuItemField/
@@ -429,6 +474,9 @@ function EventPage({ params }: { params: Promise<{ slug: string }> }) {
429474
}
430475
}}
431476
/>
477+
{touched.items && fieldErrors.items && (
478+
<p className="text-xs text-red-500 mt-1.5">{fieldErrors.items}</p>
479+
)}
432480
</CardContent>
433481
)}
434482
</Card>
@@ -559,35 +607,51 @@ function toKebabCase(string: string) {
559607
.replace(/^-+|-+$/g, "");
560608
}
561609

562-
function isFormInvalid(internalEvent: Event) {
563-
return (
564-
internalEvent.name?.length < 4 ||
565-
internalEvent.name?.length > 20 ||
566-
internalEvent.maxOrders < 1 ||
567-
internalEvent.pickupLocation.length < 3 ||
568-
internalEvent.senders?.length! < 1 ||
569-
internalEvent.selection.items?.length < 2
570-
);
571-
}
610+
type EventFieldErrors = {
611+
name?: string;
612+
maxOrders?: string;
613+
pickupLocation?: string;
614+
senders?: string;
615+
items?: string;
616+
};
617+
618+
// Single source of truth for validation, shared by the per-field inline
619+
// messages and the Create Event button's tooltip.
620+
function getFieldErrors(internalEvent: Event): EventFieldErrors {
621+
const errors: EventFieldErrors = {};
572622

573-
function validateButtonTooltip(internalEvent: Event): string {
574623
if (internalEvent.name?.length < 4) {
575-
return "Event name must be at least 4 characters long";
576-
}
577-
if (internalEvent.name?.length > 20) {
578-
return "Event name must be 20 characters long or less";
624+
errors.name = "Event name must be at least 4 characters long";
625+
} else if (internalEvent.name?.length > 20) {
626+
errors.name = "Event name must be 20 characters long or less";
579627
}
580628
if (internalEvent.maxOrders < 1) {
581-
return "Max orders must be at least 1";
629+
errors.maxOrders = "Max orders must be at least 1";
582630
}
583631
if (internalEvent.pickupLocation.length < 3) {
584-
return "Pickup location must be at least 3 characters long";
632+
errors.pickupLocation = "Pickup location must be at least 3 characters long";
585633
}
586634
if (internalEvent.senders?.length! < 1) {
587-
return "At least one sender must be selected";
635+
errors.senders = "At least one sender must be selected";
588636
}
589637
if (internalEvent.selection.items?.length < 2) {
590-
return "At least two menu items must be selected";
638+
errors.items = "At least two menu items must be selected";
591639
}
592-
return "Create Event";
640+
return errors;
641+
}
642+
643+
function isFormInvalid(internalEvent: Event) {
644+
return Object.keys(getFieldErrors(internalEvent)).length > 0;
645+
}
646+
647+
function validateButtonTooltip(internalEvent: Event): string {
648+
const errors = getFieldErrors(internalEvent);
649+
return (
650+
errors.name ??
651+
errors.maxOrders ??
652+
errors.pickupLocation ??
653+
errors.senders ??
654+
errors.items ??
655+
"Create Event"
656+
);
593657
}

0 commit comments

Comments
 (0)