Skip to content

Commit e4374a4

Browse files
authored
Implement handling for new iCard swiper (#384)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## New Features * Item filter selections in the ticket scanning interface are now reflected in the URL, enabling shareable links with pre-selected items * URL parameters are validated against loaded data, with invalid selections automatically cleared for consistency <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 3bdd0f4 commit e4374a4

File tree

1 file changed

+81
-39
lines changed

1 file changed

+81
-39
lines changed

src/ui/pages/tickets/ScanTickets.page.tsx

Lines changed: 81 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ import {
1717
} from "@mantine/core";
1818
import { IconAlertCircle, IconCheck, IconCamera } from "@tabler/icons-react";
1919
import jsQR from "jsqr";
20-
import React, { useEffect, useState, useRef } from "react";
20+
// **MODIFIED**: Added useCallback
21+
import React, { useEffect, useState, useRef, useCallback } from "react";
22+
// **NEW**: Import useSearchParams to manage URL state
23+
import { useSearchParams } from "react-router-dom";
2124

2225
import FullScreenLoader from "@ui/components/AuthContext/LoadingScreen";
2326
import { AuthGuard } from "@ui/components/AuthGuard";
@@ -112,6 +115,9 @@ const ScanTicketsPageInternal: React.FC<ScanTicketsPageProps> = ({
112115
checkInTicket: checkInTicketProp,
113116
getEmailFromUIN: getEmailFromUINProp,
114117
}) => {
118+
// **NEW**: Initialize searchParams hooks
119+
const [searchParams, setSearchParams] = useSearchParams();
120+
115121
const [orgList, setOrgList] = useState<string[] | null>(null);
116122
const [showModal, setShowModal] = useState(false);
117123
const [scanResult, setScanResult] = useState<APIResponseSchema | null>(null);
@@ -142,8 +148,9 @@ const ScanTicketsPageInternal: React.FC<ScanTicketsPageProps> = ({
142148
group: string;
143149
items: Array<{ value: string; label: string }>;
144150
}> | null>(null);
151+
// **NEW**: Read initial value from URL search param "itemId"
145152
const [selectedItemFilter, setSelectedItemFilter] = useState<string | null>(
146-
null,
153+
searchParams.get("itemId") || null,
147154
);
148155
// **NEW**: State to hold the mapping of productId to friendly name
149156
const [productNameMap, setProductNameMap] = useState<Map<string, string>>(
@@ -159,58 +166,66 @@ const ScanTicketsPageInternal: React.FC<ScanTicketsPageProps> = ({
159166
const isScanningRef = useRef(false); // Use ref for immediate updates
160167
const manualInputRef = useRef<HTMLInputElement | null>(null);
161168

162-
// Default API functions
163169
const getOrganizations =
164170
getOrganizationsProp ||
165-
(async () => {
171+
useCallback(async () => {
166172
const response = await api.get("/api/v1/organizations");
167173
return response.data;
168-
});
174+
}, [api]);
169175

170176
const getTicketItems =
171177
getTicketItemsProp ||
172-
(async () => {
178+
useCallback(async () => {
173179
const response = await api.get("/api/v1/tickets");
174180
return response.data;
175-
});
181+
}, [api]);
176182

177183
const getPurchasesByEmail =
178184
getPurchasesByEmailProp ||
179-
(async (email: string) => {
180-
const response = await api.get<PurchasesByEmailResponse>(
181-
`/api/v1/tickets/purchases/${encodeURIComponent(email)}`,
182-
);
183-
return response.data;
184-
});
185+
useCallback(
186+
async (email: string) => {
187+
const response = await api.get<PurchasesByEmailResponse>(
188+
`/api/v1/tickets/purchases/${encodeURIComponent(email)}`,
189+
);
190+
return response.data;
191+
},
192+
[api],
193+
);
185194

186195
const checkInTicket =
187196
checkInTicketProp ||
188-
(async (data: any) => {
189-
const response = await api.post(
190-
`/api/v1/tickets/checkIn`,
191-
recursiveToCamel(data),
192-
);
193-
return response.data as APIResponseSchema;
194-
});
197+
useCallback(
198+
async (data: any) => {
199+
const response = await api.post(
200+
`/api/v1/tickets/checkIn`,
201+
recursiveToCamel(data),
202+
);
203+
return response.data as APIResponseSchema;
204+
},
205+
[api],
206+
);
195207

196-
const getEmailFromUINDefault = async (uin: string): Promise<string> => {
197-
try {
198-
const response = await api.post(`/api/v1/users/findUserByUin`, { uin });
199-
return response.data.email;
200-
} catch (error: any) {
201-
const samp = new ValidationError({
202-
message: "Failed to convert UIN to email.",
203-
});
204-
if (
205-
error.response?.status === samp.httpStatusCode &&
206-
error.response?.data.id === samp.id
207-
) {
208-
const validationData = error.response.data;
209-
throw new ValidationError(validationData.message || samp.message);
208+
const getEmailFromUINDefault = useCallback(
209+
async (uin: string): Promise<string> => {
210+
try {
211+
const response = await api.post(`/api/v1/users/findUserByUin`, { uin });
212+
return response.data.email;
213+
} catch (error: any) {
214+
const samp = new ValidationError({
215+
message: "Failed to convert UIN to email.",
216+
});
217+
if (
218+
error.response?.status === samp.httpStatusCode &&
219+
error.response?.data.id === samp.id
220+
) {
221+
const validationData = error.response.data;
222+
throw new ValidationError(validationData.message || samp.message);
223+
}
224+
throw error;
210225
}
211-
throw error;
212-
}
213-
};
226+
},
227+
[api],
228+
);
214229

215230
const getEmailFromUIN = getEmailFromUINProp || getEmailFromUINDefault;
216231

@@ -346,6 +361,18 @@ const ScanTicketsPageInternal: React.FC<ScanTicketsPageProps> = ({
346361
}
347362

348363
setTicketItems(groups);
364+
365+
// After loading items, validate the item from the URL
366+
const itemIdFromUrl = searchParams.get("itemId");
367+
if (itemIdFromUrl) {
368+
const allItems = groups.flatMap((g) => g.items);
369+
if (allItems.some((item) => item.value === itemIdFromUrl)) {
370+
setSelectedItemFilter(itemIdFromUrl);
371+
} else {
372+
setSelectedItemFilter(null);
373+
setSearchParams({}, { replace: true });
374+
}
375+
}
349376
} catch (err) {
350377
console.error("Failed to fetch ticket items:", err);
351378
setTicketItems([]);
@@ -363,7 +390,8 @@ const ScanTicketsPageInternal: React.FC<ScanTicketsPageProps> = ({
363390
cancelAnimationFrame(animationFrameId.current);
364391
}
365392
};
366-
}, []);
393+
// **MODIFIED**: Added dependencies to useEffect
394+
}, [getOrganizations, getTicketItems, searchParams, setSearchParams]);
367395

368396
const processVideoFrame = async (
369397
video: HTMLVideoElement,
@@ -801,6 +829,19 @@ const ScanTicketsPageInternal: React.FC<ScanTicketsPageProps> = ({
801829
setShowModal(true); // Show the main modal with results
802830
};
803831

832+
// **NEW**: Memoize the onChange handler for the item filter Select
833+
const handleItemFilterChange = useCallback(
834+
(value: string | null) => {
835+
setSelectedItemFilter(value);
836+
if (value) {
837+
setSearchParams({ itemId: value }, { replace: true });
838+
} else {
839+
setSearchParams({}, { replace: true });
840+
}
841+
},
842+
[setSearchParams], // setSearchParams is stable
843+
);
844+
804845
if (orgList === null || ticketItems === null) {
805846
return <FullScreenLoader />;
806847
}
@@ -819,7 +860,8 @@ const ScanTicketsPageInternal: React.FC<ScanTicketsPageProps> = ({
819860
placeholder="Select an event or item to begin"
820861
data={ticketItems}
821862
value={selectedItemFilter}
822-
onChange={setSelectedItemFilter}
863+
// **MODIFIED**: Use the memoized handler
864+
onChange={handleItemFilterChange}
823865
searchable
824866
disabled={isLoading}
825867
w="100%"

0 commit comments

Comments
 (0)