1class EventbriteEvents extends HTMLElement { 2 constructor() { 3 super(); 4 // Configuration will be read in connectedCallback when attributes are available 5 this.organizerId = ''; 6 this.apiToken = ''; 7 this.maxEvents = 6; 8 this.proxyUrl = '/api/eventbrite'; 9 } 10 11 connectedCallback() { 12 // Read attributes when component is connected to DOM 13 this.organizerId = this.getAttribute('organizer-id') || ''; 14 this.apiToken = this.getAttribute('api-token') || ''; 15 this.maxEvents = parseInt(this.getAttribute('max-events')) || 6; 16 17 // Auto-detect environment and set appropriate proxy URL 18 this.proxyUrl = this.getProxyUrl(); 19 20 console.log('EventbriteEvents component connected', { 21 organizerId: this.organizerId, 22 apiToken: this.apiToken ? 'present' : 'missing', 23 proxyUrl: this.proxyUrl, 24 maxEvents: this.maxEvents, 25 environment: this.getEnvironment() 26 }); 27 28 this.render(); 29 if (this.organizerId) { 30 this.loadEvents(); 31 } else { 32 console.error('No organizer ID provided'); 33 } 34 } 35 36 getEnvironment() { 37 const hostname = window.location.hostname; 38 if (hostname === 'localhost' || hostname === '127.0.0.1') { 39 return 'local'; 40 } else if (hostname.includes('netlify.app') || hostname.includes('netlify.com')) { 41 return 'netlify'; 42 } else { 43 return 'production'; 44 } 45 } 46 47 getProxyUrl() { 48 // Check if proxy-url is explicitly set in HTML 49 const explicitUrl = this.getAttribute('proxy-url'); 50 if (explicitUrl) { 51 return explicitUrl; 52 } 53 54 // Auto-detect based on environment 55 const environment = this.getEnvironment(); 56 57 switch (environment) { 58 case 'local': 59 return 'http://localhost:3001/api/eventbrite'; 60 case 'netlify': 61 case 'production': 62 default: 63 return '/.netlify/functions/eventbrite'; 64 } 65 } 66 67 render() { 68 this.innerHTML = ` 69 <div class="eventbrite-container"> 70 <div id="events-grid" class="grid md:grid-cols-2 lg:grid-cols-3 gap-6"> 71 ${this.getLoadingPlaceholders()} 72 </div> 73 </div> 74 `; 75 } 76 77 getLoadingPlaceholders() { 78 return Array(3).fill('').map(() => ` 79 <div class="animate-pulse"> 80 <div class="bg-gray-200 h-48 rounded-t-lg"></div> 81 <div class="p-4 border border-gray-200 rounded-b-lg"> 82 <div class="h-4 bg-gray-200 rounded mb-2"></div> 83 <div class="h-4 bg-gray-200 rounded w-3/4 mb-2"></div> 84 <div class="h-3 bg-gray-200 rounded w-1/2"></div> 85 </div> 86 </div> 87 `).join(''); 88 } 89 90 async loadEvents() { 91 console.log('Loading events...'); 92 const container = this.querySelector('#events-grid'); 93 94 try { 95 // For development - using direct API (will face CORS issues) 96 // For production - use a backend proxy endpoint 97 const events = await this.fetchEventsDirectly(); 98 console.log('Events fetched:', events); 99 100 if (events && events.length > 0) { 101 container.innerHTML = events.map(event => this.createEventCard(event)).join(''); 102 } else { 103 container.innerHTML = this.getNoEventsMessage(); 104 } 105 } catch (error) { 106 console.error('Error loading Eventbrite events:', error); 107 container.innerHTML = this.getFallbackMessage(); 108 } 109 } 110 111 async fetchEventsDirectly() { 112 // Check if proxy URL is configured 113 if (this.proxyUrl && this.proxyUrl !== '/api/eventbrite') { 114 console.log('Using proxy URL:', this.proxyUrl); 115 return this.fetchEventsFromProxy(); 116 } 117 118 // Fallback to direct API (will face CORS issues) 119 console.warn('No proxy configured, attempting direct API call (may fail due to CORS)'); 120 const url = `https://www.eventbriteapi.com/v3/organizers/${this.organizerId}/events/?status=live&order_by=start_asc&expand=venue`; 121 122 try { 123 const response = await fetch(url, { 124 headers: { 125 'Authorization': `Bearer ${this.apiToken}`, 126 } 127 }); 128 129 if (!response.ok) throw new Error('Failed to fetch events'); 130 131 const data = await response.json(); 132 return data.events.slice(0, this.maxEvents); 133 } catch (error) { 134 console.warn('Direct API failed, using mock data:', error.message); 135 return this.getMockEvents(); 136 } 137 } 138 139 async fetchEventsFromProxy() { 140 try { 141 const url = `${this.proxyUrl}?organizerId=${this.organizerId}`; 142 console.log('Fetching from proxy:', url); 143 144 const response = await fetch(url); 145 146 if (!response.ok) { 147 const errorData = await response.json().catch(() => ({ error: 'Unknown error' })); 148 throw new Error(`Proxy error: ${response.status} - ${errorData.error || 'Unknown error'}`); 149 } 150 151 const data = await response.json(); 152 return data.events ? data.events.slice(0, this.maxEvents) : []; 153 } catch (error) { 154 console.error('Proxy fetch failed:', error); 155 throw error; 156 } 157 } 158 159 createEventCard(event) { 160 const startDate = new Date(event.start.local || event.start.utc); 161 const formattedDate = this.formatDate(startDate); 162 const formattedTime = this.formatTime(startDate); 163 164 return ` 165 <div class="bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow overflow-hidden"> 166 ${event.logo ? ` 167 <div class="h-48 bg-gray-100"> 168 <img src="${event.logo.url}" alt="${event.name.text}" class="w-full h-full object-cover"> 169 </div> 170 ` : ` 171 <div class="h-48 bg-gradient-to-br from-orange-400 to-orange-600 flex items-center justify-center">
172 <span class="text-white font-heading text-3xl">FIT</span> 173 </div> 174 `} 175 <div class="p-4"> 176 <h3 class="font-heading text-xl mb-2 line-clamp-2"> 177 ${event.name.text} 178 </h3> 179 <div class="text-gray-600 text-sm font-body mb-3"> 180 <div class="flex items-center mb-1"> 181 <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 182 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path> 183 </svg> 184 ${formattedDate} 185 </div> 186 <div class="flex items-center mb-1"> 187 <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 188 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path> 189 </svg> 190 ${formattedTime} 191 </div> 192 ${event.venue ? ` 193 <div class="flex items-center"> 194 <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 195 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path> 196 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path> 197 </svg> 198 ${event.venue.name || event.venue.address.city || 'Online'} 199 </div> 200 ` : ''} 201 </div> 202 <a href="${event.url}" 203 target="_blank" 204 class="inline-block w-full text-center px-4 py-2 font-heading tracking-wider rounded-sm text-white bg-orange-500 hover:bg-orange-600 transition-colors"> 205 Register Now 206 </a> 207 </div> 208 </div> 209 `; 210 } 211 212 formatDate(date) { 213 const options = { month: 'long', day: 'numeric', year: 'numeric' }; 214 return date.toLocaleDateString('en-US', options); 215 } 216 217 formatTime(date) { 218 const options = { hour: 'numeric', minute: '2-digit', timeZoneName: 'short' }; 219 return date.toLocaleTimeString('en-US', options); 220 } 221 222 getNoEventsMessage() { 223 return ` 224 <div class="col-span-full text-center py-12"> 225 <svg class="w-16 h-16 mx-auto text-gray-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 226 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path> 227 </svg> 228 <h3 class="text-xl font-heading mb-2">No Upcoming Events</h3> 229 <p class="text-gray-600 font-body mb-4">Check back soon for new training opportunities.</p> 230 <a href="mailto:[email protected]" 231 class="inline-block px-6 py-2 font-heading tracking-wider rounded-sm text-white bg-orange-500 hover:bg-orange-600 transition-colors"> 232 Contact for Private Training 233 </a> 234 </div> 235 `; 236 } 237 238 getFallbackMessage() { 239 return ` 240 <div class="col-span-full text-center py-8"> 241 <p class="text-gray-600 font-body mb-4"> 242 Unable to load events at this time. 243 </p> 244 <a href="https://www.eventbrite.com/o/${this.organizerId}" 245 target="_blank" 246 class="inline-block px-6 py-2 font-heading tracking-wider rounded-sm text-orange-500 border border-orange-500 hover:bg-orange-500 hover:text-white transition-colors"> 247 View Events on Eventbrite 248 </a> 249 </div> 250 `; 251 } 252 253 getMockEvents() { 254 // Mock data for development 255 return [ 256 { 257 name: { text: "Introduction to Feedback Informed Treatment" }, 258 start: { local: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString() }, 259 url: "https://www.eventbrite.com/e/example", 260 venue: { name: "Online Workshop" }, 261 logo: null 262 }, 263 { 264 name: { text: "Advanced FIT Implementation Strategies" }, 265 start: { local: new Date(Date.now() + 45 * 24 * 60 * 60 * 1000).toISOString() }, 266 url: "https://www.eventbrite.com/e/example", 267 venue: { name: "Chicago, IL" }, 268 logo: null 269 }, 270 { 271 name: { text: "Deliberate Practice for Therapists" }, 272 start: { local: new Date(Date.now() + 60 * 24 * 60 * 60 * 1000).toISOString() }, 273 url: "https://www.eventbrite.com/e/example", 274 venue: { name: "New York, NY" }, 275 logo: null 276 } 277 ]; 278 } 279} 280 281// Define the custom element 282customElements.define('eventbrite-events', EventbriteEvents);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.