|
1 | | -import fs from 'fs'; |
2 | | -import Parser from 'rss-parser'; |
3 | | -import axios from 'axios'; |
| 1 | +import fs from "fs"; |
| 2 | +import Parser from "rss-parser"; |
| 3 | +import axios from "axios"; |
| 4 | + |
| 5 | +import { fileURLToPath } from "url"; |
| 6 | +import path from "path"; |
4 | 7 |
|
5 | 8 | const parser = new Parser(); |
6 | 9 |
|
7 | 10 | async function fetchMeetupEvents() { |
8 | | - try { |
9 | | - console.log('📅 Fetching Meetup events via RSS feed...'); |
10 | | - const feed = await parser.parseURL('https://www.meetup.com/lighthouselive//events/rss/'); |
11 | | - |
12 | | - const events = []; |
13 | | - |
14 | | - const dateRegex = /(\b\d{2})\.(\d{2})\.(\d{4})\b/; // DD.MM.YYYY |
15 | | - const timeRegex = /(\b\d{1,2}:\d{2}\b)/; // HH:MM |
16 | | - |
17 | | - for (const item of feed.items) { |
18 | | - const content = item.contentSnippet || item.content || ''; |
19 | | - |
20 | | - let eventDate = null; |
21 | | - const dateMatch = content.match(dateRegex); |
22 | | - if (dateMatch) { |
23 | | - const [_, day, month, year] = dateMatch; |
24 | | - eventDate = new Date(`${year}-${month}-${day}T00:00:00`); |
25 | | - if (eventDate <= new Date()) continue; // filter past events |
26 | | - } |
27 | | - |
28 | | - let timeText = ''; |
29 | | - const timeMatch = content.match(timeRegex); |
30 | | - if (timeMatch) timeText = timeMatch[0]; |
31 | | - |
32 | | - events.push({ |
33 | | - type: "Meetup", |
34 | | - title: item.title, |
35 | | - date: eventDate ? eventDate.toISOString() : null, |
36 | | - displayDate: eventDate ? formatDate(eventDate) : 'TBD', |
37 | | - time: timeText, |
38 | | - location: 'Online', |
39 | | - price: 'Free', |
40 | | - description: content.substring(0, 300) + '...', |
41 | | - ctaText: 'RSVP on Meetup', |
42 | | - ctaLink: item.link, |
43 | | - source: 'meetup' |
44 | | - }); |
45 | | - } |
46 | | - |
47 | | - console.log(`✅ Fetched ${events.length} Meetup events`); |
48 | | - return events; |
49 | | - } catch (error) { |
50 | | - console.error('❌ Meetup RSS error:', error.message); |
51 | | - return []; |
52 | | - } |
| 11 | + try { |
| 12 | + console.log("📅 Fetching Meetup events via RSS feed..."); |
| 13 | + const feed = await parser.parseURL( |
| 14 | + "https://www.meetup.com/lighthouselive//events/rss/", |
| 15 | + ); |
| 16 | + |
| 17 | + const events = []; |
| 18 | + |
| 19 | + const dateRegex = /(\b\d{2})\.(\d{2})\.(\d{4})\b/; // DD.MM.YYYY |
| 20 | + const timeRegex = /(\b\d{1,2}:\d{2}\b)/; // HH:MM |
| 21 | + |
| 22 | + for (const item of feed.items) { |
| 23 | + const content = item.contentSnippet || item.content || ""; |
| 24 | + |
| 25 | + let eventDate = null; |
| 26 | + const dateMatch = content.match(dateRegex); |
| 27 | + if (dateMatch) { |
| 28 | + const [_, day, month, year] = dateMatch; |
| 29 | + eventDate = new Date(`${year}-${month}-${day}T00:00:00`); |
| 30 | + if (eventDate <= new Date()) continue; // filter past events |
| 31 | + } |
| 32 | + |
| 33 | + let timeText = ""; |
| 34 | + const timeMatch = content.match(timeRegex); |
| 35 | + if (timeMatch) timeText = timeMatch[0]; |
| 36 | + |
| 37 | + events.push({ |
| 38 | + type: "Meetup", |
| 39 | + title: item.title, |
| 40 | + date: eventDate ? eventDate.toISOString() : null, |
| 41 | + displayDate: eventDate ? formatDate(eventDate) : "TBD", |
| 42 | + time: timeText, |
| 43 | + location: "Online", |
| 44 | + price: "Free", |
| 45 | + description: content.substring(0, 300) + "...", |
| 46 | + ctaText: "RSVP on Meetup", |
| 47 | + ctaLink: item.link, |
| 48 | + source: "meetup", |
| 49 | + }); |
| 50 | + } |
| 51 | + |
| 52 | + console.log(`✅ Fetched ${events.length} Meetup events`); |
| 53 | + return events; |
| 54 | + } catch (error) { |
| 55 | + console.error("❌ Meetup RSS error:", error.message); |
| 56 | + return []; |
| 57 | + } |
53 | 58 | } |
54 | 59 |
|
55 | 60 | async function fetchTicketTailorEvents() { |
56 | | - try { |
57 | | - const TICKET_TAILOR_API_KEY = process.env.TICKET_TAILOR_API_KEY; |
58 | | - if (!TICKET_TAILOR_API_KEY) return []; |
59 | | - |
60 | | - const response = await axios.get('https://api.tickettailor.com/v1/events', { |
61 | | - params: { status: 'published', start_at_gte: new Date().toISOString() }, |
62 | | - headers: { Authorization: `Basic ${Buffer.from(TICKET_TAILOR_API_KEY + ':').toString('base64')}`, Accept: 'application/json' } |
63 | | - }); |
64 | | - |
65 | | - const now = new Date(); |
66 | | - return response.data.data |
67 | | - .filter(event => new Date(event.start.date) > now) |
68 | | - .map(event => { |
69 | | - const eventDate = new Date(event.start.date); |
70 | | - const prices = event.ticket_types?.map(t => t.price / 100) || []; |
71 | | - const minPrice = prices.length > 0 ? Math.min(...prices) : null; |
72 | | - const maxPrice = prices.length > 0 ? Math.max(...prices) : null; |
73 | | - |
74 | | - // Map currency codes to symbols |
75 | | - const currencySymbol = { |
76 | | - 'eur': '€', |
77 | | - 'usd': '$', |
78 | | - 'chf': 'CHF' |
79 | | - }[event.currency?.toLowerCase()] || '$'; |
80 | | - |
81 | | - let priceDisplay = 'Free'; |
82 | | - if (minPrice && maxPrice) priceDisplay = minPrice === maxPrice ? `${currencySymbol}${minPrice}` : `${currencySymbol}${minPrice} - ${currencySymbol}${maxPrice}`; |
83 | | - |
84 | | - return { |
85 | | - type: determineEventType(event.description), |
86 | | - title: event.name, |
87 | | - date: event.start.date, |
88 | | - displayDate: formatDate(eventDate), |
89 | | - time: `${formatTime(eventDate)} - ${formatTime(new Date(event.end.date))} ${event.start.timezone}`, |
90 | | - location: event.venue?.name || 'Online', |
91 | | - price: priceDisplay, |
92 | | - description: stripHtml(event.description).substring(0, 300) + '...', |
93 | | - ctaText: 'Buy Tickets', |
94 | | - ctaLink: event.url, |
95 | | - source: 'tickettailor' |
96 | | - }; |
97 | | - }); |
98 | | - } catch (error) { |
99 | | - console.error('❌ Ticket Tailor API error:', error.message); |
100 | | - return []; |
101 | | - } |
| 61 | + try { |
| 62 | + const TICKET_TAILOR_API_KEY = process.env.TICKET_TAILOR_API_KEY; |
| 63 | + if (!TICKET_TAILOR_API_KEY) return []; |
| 64 | + |
| 65 | + const response = await axios.get("https://api.tickettailor.com/v1/events", { |
| 66 | + params: { status: "published", start_at_gte: new Date().toISOString() }, |
| 67 | + headers: { |
| 68 | + Authorization: `Basic ${Buffer.from(TICKET_TAILOR_API_KEY + ":").toString("base64")}`, |
| 69 | + Accept: "application/json", |
| 70 | + }, |
| 71 | + }); |
| 72 | + |
| 73 | + const now = new Date(); |
| 74 | + return response.data.data |
| 75 | + .filter((event) => new Date(event.start.date) > now) |
| 76 | + .map((event) => { |
| 77 | + const eventDate = new Date(event.start.date); |
| 78 | + const prices = event.ticket_types?.map((t) => t.price / 100) || []; |
| 79 | + const minPrice = prices.length > 0 ? Math.min(...prices) : null; |
| 80 | + const maxPrice = prices.length > 0 ? Math.max(...prices) : null; |
| 81 | + |
| 82 | + // Map currency codes to symbols |
| 83 | + const currencySymbol = |
| 84 | + { |
| 85 | + eur: "€", |
| 86 | + usd: "$", |
| 87 | + chf: "CHF", |
| 88 | + }[event.currency?.toLowerCase()] || "$"; |
| 89 | + |
| 90 | + let priceDisplay = "Free"; |
| 91 | + if (minPrice && maxPrice) |
| 92 | + priceDisplay = |
| 93 | + minPrice === maxPrice |
| 94 | + ? `${currencySymbol}${minPrice}` |
| 95 | + : `${currencySymbol}${minPrice} - ${currencySymbol}${maxPrice}`; |
| 96 | + |
| 97 | + return { |
| 98 | + type: determineEventType(event.description), |
| 99 | + title: event.name, |
| 100 | + date: event.start.date, |
| 101 | + displayDate: formatDate(eventDate), |
| 102 | + time: `${formatTime(eventDate)} - ${formatTime(new Date(event.end.date))} ${event.start.timezone}`, |
| 103 | + location: event.venue?.name || "Online", |
| 104 | + price: priceDisplay, |
| 105 | + description: stripHtml(event.description).substring(0, 300) + "...", |
| 106 | + ctaText: "Buy Tickets", |
| 107 | + ctaLink: event.url, |
| 108 | + source: "tickettailor", |
| 109 | + }; |
| 110 | + }); |
| 111 | + } catch (error) { |
| 112 | + console.error("❌ Ticket Tailor API error:", error.message); |
| 113 | + return []; |
| 114 | + } |
102 | 115 | } |
103 | 116 |
|
104 | 117 | function determineEventType(title) { |
105 | | - const titleLower = title.toLowerCase(); |
106 | | - if (titleLower.includes('workshop')) return ['Workshop']; |
107 | | - if (titleLower.includes('training') || titleLower.includes('course')) return ['Training']; |
108 | | - if (titleLower.includes('talk') || titleLower.includes('presentation')) return ['Talk']; |
109 | | - if (titleLower.includes('conference') || titleLower.includes('summit')) return ['Conference']; |
110 | | - return ['Talk']; |
| 118 | + const titleLower = title.toLowerCase(); |
| 119 | + if (titleLower.includes("workshop")) return ["Workshop"]; |
| 120 | + if (titleLower.includes("training") || titleLower.includes("course")) |
| 121 | + return ["Training"]; |
| 122 | + if (titleLower.includes("talk") || titleLower.includes("presentation")) |
| 123 | + return ["Talk"]; |
| 124 | + if (titleLower.includes("conference") || titleLower.includes("summit")) |
| 125 | + return ["Conference"]; |
| 126 | + return ["Talk"]; |
111 | 127 | } |
112 | 128 |
|
113 | 129 | function formatDate(date) { |
114 | | - return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); |
| 130 | + return date.toLocaleDateString("en-US", { |
| 131 | + month: "short", |
| 132 | + day: "numeric", |
| 133 | + year: "numeric", |
| 134 | + }); |
115 | 135 | } |
116 | 136 |
|
117 | 137 | function formatTime(date) { |
118 | | - return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }); |
| 138 | + return date.toLocaleTimeString("en-US", { |
| 139 | + hour: "numeric", |
| 140 | + minute: "2-digit", |
| 141 | + hour12: true, |
| 142 | + }); |
119 | 143 | } |
120 | 144 |
|
121 | 145 | function stripHtml(html) { |
122 | | - if (!html) return ''; |
123 | | - return html.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim(); |
| 146 | + if (!html) return ""; |
| 147 | + return html |
| 148 | + .replace(/<[^>]*>/g, "") |
| 149 | + .replace(/\s+/g, " ") |
| 150 | + .trim(); |
124 | 151 | } |
125 | 152 |
|
126 | 153 | async function main() { |
127 | | - console.log('🚀 Starting event sync...\n'); |
128 | | - |
129 | | - const [meetupEvents, ticketTailorEvents] = await Promise.all([fetchMeetupEvents(), fetchTicketTailorEvents()]); |
130 | | - const allEvents = [...meetupEvents, ...ticketTailorEvents].sort((a, b) => { |
131 | | - if (!a.date) return 1; |
132 | | - if (!b.date) return -1; |
133 | | - return new Date(a.date) - new Date(b.date); |
134 | | - }); |
135 | | - |
136 | | - const output = { events: allEvents }; |
137 | | - fs.writeFileSync('public/events-data.json', JSON.stringify(output, null, 2)); |
138 | | - |
139 | | - console.log(`\n✨ Successfully synced ${allEvents.length} total events`); |
140 | | - console.log(` - Meetup: ${meetupEvents.length}`); |
141 | | - console.log(` - Ticket Tailor: ${ticketTailorEvents.length}`); |
| 154 | + console.log("🚀 Starting event sync...\n"); |
| 155 | + |
| 156 | + const [meetupEvents, ticketTailorEvents] = await Promise.all([ |
| 157 | + fetchMeetupEvents(), |
| 158 | + fetchTicketTailorEvents(), |
| 159 | + ]); |
| 160 | + const allEvents = [...meetupEvents, ...ticketTailorEvents].sort((a, b) => { |
| 161 | + if (!a.date) return 1; |
| 162 | + if (!b.date) return -1; |
| 163 | + return new Date(a.date) - new Date(b.date); |
| 164 | + }); |
| 165 | + |
| 166 | + const output = { events: allEvents }; |
| 167 | + const __dirname = path.dirname(fileURLToPath(import.meta.url)); |
| 168 | + fs.writeFileSync( |
| 169 | + path.join(__dirname, "../public/events-data.json"), |
| 170 | + JSON.stringify(output, null, 2), |
| 171 | + ); |
| 172 | + |
| 173 | + console.log(`\n✨ Successfully synced ${allEvents.length} total events`); |
| 174 | + console.log(` - Meetup: ${meetupEvents.length}`); |
| 175 | + console.log(` - Ticket Tailor: ${ticketTailorEvents.length}`); |
142 | 176 | } |
143 | 177 |
|
144 | | -main().catch(error => { |
145 | | - console.error('❌ Fatal error:', error); |
146 | | - process.exit(1); |
| 178 | +main().catch((error) => { |
| 179 | + console.error("❌ Fatal error:", error); |
| 180 | + process.exit(1); |
147 | 181 | }); |
0 commit comments