From c52837734183e1e1114f10042fce7fe615650d18 Mon Sep 17 00:00:00 2001 From: Kamil Dzieniszewski Date: Thu, 5 Feb 2026 16:59:20 +0100 Subject: [PATCH] feat: local meetups --- .github/workflows/deploy.yml | 8 +- public/meetups.json | 85 ++++++++++ scripts/fetch-meetups.js | 37 ++++ src/App.tsx | 2 + src/components/monthly-meetups.tsx | 260 +++++++++++++++++++++++++++++ 5 files changed, 391 insertions(+), 1 deletion(-) create mode 100644 public/meetups.json create mode 100644 scripts/fetch-meetups.js create mode 100644 src/components/monthly-meetups.tsx diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 02aa9a99..738777a1 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,7 +2,7 @@ name: Deploy to GitHub Pages on: push: - branches: [ '2026' ] + branches: ['2026'] permissions: contents: write @@ -29,6 +29,12 @@ jobs: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - run: pnpm install --frozen-lockfile + + - name: Fetch meetups data + run: node scripts/fetch-meetups.js + env: + MEETUPS_API_URL: ${{ secrets.MEETUPS_API_URL }} + - run: pnpm build - uses: peaceiris/actions-gh-pages@v4 diff --git a/public/meetups.json b/public/meetups.json new file mode 100644 index 00000000..918dce28 --- /dev/null +++ b/public/meetups.json @@ -0,0 +1,85 @@ +{ + "1": { + "id": 64570, + "date_add": 1768395416, + "date": "5.02.2026", + "time": "17:30", + "name": "meet.js Gdansk TypeScript Meetup #18", + "type": "Spotkanie", + "url": "https://crossweb.pl/wydarzenia/meetjs-gdansk-typescript-meetup-18/", + "rsvp": "https://www.meetup.com/gdansk-typescript/events/312877678/?eventOrigin=group_upcoming_events", + "city": "Trójmiasto", + "address": "aleja Grunwaldzka 411", + "image": "https://crossweb.pl/upload/gallery/event/64570/840x320/-1768997061.png", + "serie": "meet.js", + "topic": [ + "JavaScript", + "Open Source", + "TypeScript" + ] + }, + "2": { + "id": 64387, + "date_add": 1766409023, + "date": "12.02.2026", + "time": "18:00", + "name": "meet.js Wrocław 2026-02-12 (15th anniversary!)", + "type": "Spotkanie", + "url": "https://crossweb.pl/wydarzenia/meet-js-wroclaw-2026-02-12-15th-anniversary/", + "rsvp": "https://www.meetup.com/meet-js-wroclaw/events/312499657/", + "city": "Wrocław", + "address": "Włodkowica 5", + "image": "https://crossweb.pl/upload/gallery/event/64387/840x320/meet-js-wroclaw-2026-02-12-15th-anniversary-1766409024.avif", + "serie": "meet.js", + "topic": [ + "JavaScript", + "Open Source", + "TypeScript", + "programowanie", + "design" + ] + }, + "3": { + "id": 72477, + "date_add": 1769515758, + "date": "19.02.2026", + "time": "17:30", + "name": "meet.js Lublin #15", + "type": "Spotkanie", + "url": "https://crossweb.pl/wydarzenia/meetjs-lublin-15/", + "rsvp": "https://luma.com/564kxhiw", + "city": "Lublin", + "address": "Prezydenta Gabriela Narutowicza 55B", + "serie": "meet.js", + "topic": [ + "JavaScript", + "Open Source", + "TypeScript", + "programowanie", + "design" + ] + }, + "4": { + "id": 64736, + "date_add": 1769074026, + "date": "4.03.2026", + "time": "09:00", + "name": "meet.js Summit 2026", + "type": "Konferencja", + "url": "https://crossweb.pl/wydarzenia/meetjs-summit-2026/", + "rsvp": "https://summit.meetjs.pl/2026/", + "city": "Warszawa", + "address": "Aleja Niepodległości 162", + "image": "https://crossweb.pl/upload/gallery/event/64736/840x320/-1769074507.png", + "serie": "meet.js", + "topic": [ + "JavaScript", + "Open Source", + "TypeScript", + "programowanie", + "design", + "AI/ML", + "Call For Papers" + ] + } +} \ No newline at end of file diff --git a/scripts/fetch-meetups.js b/scripts/fetch-meetups.js new file mode 100644 index 00000000..78173b0f --- /dev/null +++ b/scripts/fetch-meetups.js @@ -0,0 +1,37 @@ +import { writeFileSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const MEETUPS_API_URL = process.env.MEETUPS_API_URL; + +if (!MEETUPS_API_URL) { + console.error('Error: MEETUPS_API_URL environment variable is not set'); + process.exit(1); +} + +async function fetchMeetups() { + try { + console.log('Fetching meetups data...'); + const response = await fetch(MEETUPS_API_URL); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + const outputPath = join(__dirname, '../public/meetups.json'); + writeFileSync(outputPath, JSON.stringify(data, null, 2)); + + console.log(`✓ Meetups data saved to ${outputPath}`); + console.log(`✓ Found ${Object.keys(data).length} events`); + } catch (error) { + console.error('Error fetching meetups:', error); + process.exit(1); + } +} + +fetchMeetups(); diff --git a/src/App.tsx b/src/App.tsx index 0f5b79f6..73c88350 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,7 @@ import { BecomeASponsor } from '@/components/become-a-sponsor.tsx'; import { Charity } from '@/components/charity.tsx'; import { LoveLetter } from '@/components/love-letter.tsx'; +import { MonthlyMeetups } from '@/components/monthly-meetups.tsx'; import { Partners } from '@/components/partners.tsx'; import { PhotosSlider } from '@/components/photos-slider.tsx'; import { Speakers } from '@/components/speakers.tsx'; @@ -28,6 +29,7 @@ export const App = () => { + {/**/} diff --git a/src/components/monthly-meetups.tsx b/src/components/monthly-meetups.tsx new file mode 100644 index 00000000..500d3067 --- /dev/null +++ b/src/components/monthly-meetups.tsx @@ -0,0 +1,260 @@ +import { useEffect, useState } from 'react'; + +import { OutboundLink } from '@/components/outbound-link.tsx'; +import { Wrapper } from '@/components/wrapper.tsx'; + +interface MeetupEvent { + id: string; + name: string; + city: string; + date: string; + time?: string; + url?: string; + venue?: string; + address?: string; + type?: string; +} + +export const MonthlyMeetups = () => { + const [upcomingMeetups, setUpcomingMeetups] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const parseDateDDMMYYYY = (dateString: string): Date => { + const [day, month, year] = dateString.split('.'); + return new Date(parseInt(year), parseInt(month) - 1, parseInt(day)); + }; + + useEffect(() => { + const fetchMeetups = async () => { + try { + const response = await fetch('/2026/meetups.json'); + + if (!response.ok) { + throw new Error('Failed to fetch meetups'); + } + + const data = await response.json(); + const meetupsArray = Object.values(data) as MeetupEvent[]; + + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const upcoming = meetupsArray + .filter(meetup => meetup.type === 'Spotkanie') + .filter(meetup => { + const eventDate = parseDateDDMMYYYY(meetup.date); + eventDate.setHours(0, 0, 0, 0); + return eventDate >= today; + }) + .sort( + (a, b) => + parseDateDDMMYYYY(a.date).getTime() - + parseDateDDMMYYYY(b.date).getTime(), + ) + .slice(0, 6); + + setUpcomingMeetups(upcoming); + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred'); + } finally { + setLoading(false); + } + }; + + fetchMeetups(); + }, []); + + const formatDate = (dateString: string, timeString?: string) => { + const date = parseDateDDMMYYYY(dateString); + const formattedDate = date.toLocaleDateString('pl-PL', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + }); + return timeString ? `${formattedDate}, ${timeString}` : formattedDate; + }; + + const getDaysUntil = (dateString: string): number => { + const today = new Date(); + today.setHours(0, 0, 0, 0); + const eventDate = parseDateDDMMYYYY(dateString); + eventDate.setHours(0, 0, 0, 0); + const diffTime = eventDate.getTime() - today.getTime(); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + return diffDays; + }; + + return ( +
+ +
+

+ Monthly Meetups +

+

+ Join us at our regular monthly JavaScript meetups across Poland +

+
+ + {loading && ( +
+

Loading meetups...

+
+ )} + + {error && ( +
+

Error loading meetups: {error}

+
+ )} + + {!loading && !error && upcomingMeetups.length === 0 && ( +
+

No upcoming meetups scheduled yet.

+
+ )} + + {!loading && !error && upcomingMeetups.length > 0 && ( +
+ {upcomingMeetups.map(meetup => ( +
+
+
+ + + + + + {meetup.city} + + {(() => { + const days = getDaysUntil(meetup.date); + return ( + + {days === 0 + ? 'TODAY' + : days === 1 + ? 'TOMORROW' + : `${days} DAYS`} + + ); + })()} +
+

+ {meetup.name} +

+
+ + + + {formatDate(meetup.date, meetup.time)} +
+ {meetup.address && ( +
+ + + + + {meetup.address} +
+ )} + {meetup.venue && ( +
+ + + + {meetup.venue} +
+ )} + {meetup.url && ( + + Learn More + + )} +
+
+ ))} +
+ )} + +
+

+ Want to organize a meetup in your city?{' '} + + Get in touch + +

+
+
+
+ ); +};