@@ -58,6 +58,13 @@ export default {
5858 } ) ;
5959 }
6060
61+ // Handle dynamic available_beverage_types.json endpoint
62+ // Pattern: /{festivalId}/available_beverage_types.json
63+ const availableTypesMatch = url . pathname . match ( / ^ \/ ( [ ^ \/ ] + ) \/ a v a i l a b l e _ b e v e r a g e _ t y p e s \. j s o n $ / ) ;
64+ if ( availableTypesMatch ) {
65+ return handleAvailableBeverageTypes ( availableTypesMatch [ 1 ] , request ) ;
66+ }
67+
6168 // Proxy the request to the upstream API
6269 const upstreamUrl = UPSTREAM_URL + url . pathname + url . search ;
6370
@@ -90,6 +97,99 @@ export default {
9097 } ,
9198} ;
9299
100+ /**
101+ * Dynamically discovers available beverage types for a festival
102+ * by fetching the directory listing from the upstream API
103+ *
104+ * @param {string } festivalId - The festival ID (e.g., 'cbf2025', 'cbfw2025')
105+ * @param {Request } request - The original request for CORS handling
106+ * @returns {Response } JSON response with available beverage types
107+ */
108+ async function handleAvailableBeverageTypes ( festivalId , request ) {
109+ try {
110+ // Fetch the directory listing for this festival
111+ const upstreamUrl = `${ UPSTREAM_URL } /${ festivalId } /` ;
112+ const response = await fetch ( upstreamUrl , {
113+ headers : {
114+ 'User-Agent' : 'Cambridge-Beer-Festival-App-Proxy/1.0' ,
115+ } ,
116+ } ) ;
117+
118+ if ( ! response . ok ) {
119+ return new Response ( JSON . stringify ( {
120+ error : 'Festival not found' ,
121+ festival_id : festivalId ,
122+ } ) , {
123+ status : 404 ,
124+ headers : {
125+ 'Content-Type' : 'application/json' ,
126+ ...getCorsHeaders ( request ) ,
127+ } ,
128+ } ) ;
129+ }
130+
131+ // Parse the HTML directory listing to find .json files
132+ const html = await response . text ( ) ;
133+ const beverageTypes = parseDirectoryListingForBeverageTypes ( html ) ;
134+
135+ // Return the list of available beverage types
136+ return new Response ( JSON . stringify ( {
137+ festival_id : festivalId ,
138+ available_beverage_types : beverageTypes ,
139+ timestamp : new Date ( ) . toISOString ( ) ,
140+ } ) , {
141+ status : 200 ,
142+ headers : {
143+ 'Content-Type' : 'application/json' ,
144+ 'Cache-Control' : 'public, max-age=3600' , // Cache for 1 hour
145+ ...getCorsHeaders ( request ) ,
146+ } ,
147+ } ) ;
148+ } catch ( error ) {
149+ return new Response ( JSON . stringify ( {
150+ error : 'Failed to fetch beverage types' ,
151+ message : error . message ,
152+ } ) , {
153+ status : 500 ,
154+ headers : {
155+ 'Content-Type' : 'application/json' ,
156+ ...getCorsHeaders ( request ) ,
157+ } ,
158+ } ) ;
159+ }
160+ }
161+
162+ /**
163+ * Parses an Apache-style directory listing HTML to extract beverage type JSON files
164+ *
165+ * @param {string } html - The HTML content of the directory listing
166+ * @returns {string[] } Array of beverage type names (without .json extension)
167+ */
168+ function parseDirectoryListingForBeverageTypes ( html ) {
169+ const beverageTypes = [ ] ;
170+
171+ // Match href attributes that point to .json files
172+ // Regex pattern: <a href="filename.json">
173+ const jsonFilePattern = / < a h r e f = " ( [ ^ " ] + \. j s o n ) " / gi;
174+ let match ;
175+
176+ while ( ( match = jsonFilePattern . exec ( html ) ) !== null ) {
177+ const filename = match [ 1 ] ;
178+
179+ // Skip the available_beverage_types.json itself to avoid recursion
180+ if ( filename === 'available_beverage_types.json' ) {
181+ continue ;
182+ }
183+
184+ // Remove .json extension to get the beverage type name
185+ const beverageType = filename . replace ( / \. j s o n $ / , '' ) ;
186+ beverageTypes . push ( beverageType ) ;
187+ }
188+
189+ // Sort alphabetically for consistency
190+ return beverageTypes . sort ( ) ;
191+ }
192+
93193function handleCorsPreflight ( request ) {
94194 return new Response ( null , {
95195 status : 204 ,
0 commit comments