@@ -17,7 +17,10 @@ import {
1717} from "@mantine/core" ;
1818import { IconAlertCircle , IconCheck , IconCamera } from "@tabler/icons-react" ;
1919import 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
2225import FullScreenLoader from "@ui/components/AuthContext/LoadingScreen" ;
2326import { 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