PageSourceSearch

https://www.scottdmiller.com/components/eventbrite-events.js

js scottdmiller.com collected 2026-10-02 05:00:23 UTC 12,009 bytes, 282 lines download raw bytes

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.