Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 42 additions & 8 deletions scripts/fetchMeetupData.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { writeFile } from "node:fs/promises";

const MEETUP_URL = "https://www.meetup.com/ottawa-forwardjs-meetup";
const MEETUP_ID = "28909672";

async function writeJsonToFile(filename, jsonData) {
const jsonString = JSON.stringify(jsonData, null, 2);

await writeFile(filename, jsonString + "\n", "utf8");
}

async function fetchEvents(url) {
async function getNextDataJsonFromPage(url) {
// fetch the page and get the HTML content
const response = await fetch(url);
const htmlContent = await response.text();
Expand All @@ -20,12 +23,27 @@ async function fetchEvents(url) {
.replace('<script id="__NEXT_DATA__" type="application/json">', "")
.replace("</script>", "");

// parse the data to JSON in the hackiest way possible
const parsedData = JSON.parse(JSON.parse(JSON.stringify(data)));
return data;
}

function getApolloState(parsedData) {
// get the APOLLO_STATE from the parsed json
const state = parsedData.props.pageProps.__APOLLO_STATE__;

return state;
}

function doubleParseJson(data) {
// parse the data to JSON in the hackiest way possible
const parsedData = JSON.parse(JSON.parse(JSON.stringify(data)));
return parsedData;
}

async function fetchMeetupEvents(url) {
const data = await getNextDataJsonFromPage(url);
const parsedData = doubleParseJson(data);
const state = getApolloState(parsedData);

// extract event data from the apollo state
const events = [];
for (const key of Object.keys(state)) {
Expand Down Expand Up @@ -81,16 +99,32 @@ async function fetchEvents(url) {
return events;
}

async function fetchMeetupStats(url) {
const data = await getNextDataJsonFromPage(url);
const parsedData = doubleParseJson(data);
const state = getApolloState(parsedData);

const { memberCounts, eventRatings } = state[`Group:${MEETUP_ID}`].stats;

return {
memberCount: memberCounts.all,
organizerCount: memberCounts.leadership,
averageEventRating: eventRatings.average,
totalEventRatings: eventRatings.total,
};
}

async function main() {
try {
const upcoming = await fetchEvents(
"https://www.meetup.com/ottawa-forwardjs-meetup/events/?type=upcoming",
const meetupStats = await fetchMeetupStats(MEETUP_URL);
await writeJsonToFile("./src/data/meetupStats.json", meetupStats);

const upcoming = await fetchMeetupEvents(
`${MEETUP_URL}/events/?type=upcoming`,
);
await writeJsonToFile("./src/data/upcomingEvents.json", upcoming);

const past = await fetchEvents(
"https://www.meetup.com/ottawa-forwardjs-meetup/events/?type=past",
);
const past = await fetchMeetupEvents(`${MEETUP_URL}/events/?type=past`);
// write the last 5 past events to the JSON files
await writeJsonToFile("./src/data/pastEvents.json", past.slice(0, 5));
} catch (error) {
Expand Down
1 change: 1 addition & 0 deletions src/components/Navigation.astro
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import logo from "../assets/forward.svg";
>
</li>
<li><a href="/jobs">Job Board</a></li>
<li><a href="/what-were-about">What We're About</a></li>
</ul>
</div>
</nav>
Expand Down
6 changes: 6 additions & 0 deletions src/data/meetupStats.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"memberCount": 2183,
"organizerCount": 8,
"averageEventRating": 4.78,
"totalEventRatings": 549
}
67 changes: 67 additions & 0 deletions src/data/organizers.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
[
{
"firstName": "Brian",
"lastName": "Farias Tavares",
"joined": "Jun, 2018",
"role": "Co-organizer",
"imageUrl": "https://secure.meetupstatic.com/photos/member/2/9/3/b/highres_321310555.jpeg",
"altText": "Photo of Brian Farias Tavares"
},
{
"firstName": "Eric",
"lastName": "Adamski",
"joined": "Jun, 2018",
"role": "Co-organizer",
"imageUrl": "https://secure.meetupstatic.com/photos/member/7/3/1/6/highres_257489462.jpeg",
"altText": "Photo of Eric Adamski"
},
{
"firstName": "Renee",
"lastName": "Ghattas",
"joined": "Jun, 2018",
"role": "Co-organizer",
"imageUrl": "https://secure.meetupstatic.com/photos/member/4/a/6/e/highres_316399054.jpeg",
"altText": "Photo of Renne Ghattas"
},
{
"firstName": "Simon",
"lastName": "MacDonald",
"joined": "Jun, 2018",
"role": "Co-organizer",
"imageUrl": "https://secure.meetupstatic.com/photos/member/2/b/7/e/highres_314171134.jpeg",
"altText": "Photo of Simon MacDonald"
},

{
"firstName": "Rey",
"lastName": "Riel",
"joined": "Jun, 2018",
"role": "Co-organizer",
"imageUrl": "https://secure.meetupstatic.com/photos/member/5/8/5/highres_259921413.jpeg",
"altText": "Photo of Rey Riel"
},
{
"firstName": "Matt",
"lastName": "Dupont",
"joined": "Feb, 2023",
"role": "Co-organizer",
"imageUrl": "https://secure.meetupstatic.com/photos/member/6/7/4/d/highres_277526445.jpeg",
"altText": "Photo of Matt Dupont"
},
{
"firstName": "Ameya",
"lastName": "Charnalia",
"joined": "Feb, 2023",
"role": "Co-organizer",
"imageUrl": "https://secure.meetupstatic.com/photos/member/5/b/f/f/highres_308843551.jpeg",
"altText": "Photo of Ameya Charnalia"
},
{
"firstName": "Matt",
"lastName": "Millard",
"joined": "Sep, 2023",
"role": "Co-organizer",
"imageUrl": "https://secure.meetupstatic.com/photos/member/9/d/0/3/highres_317140195.jpeg",
"altText": "Photo of Matt Millard"
}
]
174 changes: 174 additions & 0 deletions src/pages/what-were-about.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
---
import organizers from "../data/organizers.json";
import { memberCount } from "../data/meetupStats.json";
import Layout from "../layouts/Layout.astro";
---

<Layout title="ForwardJS - Who We Are">
<div class="container">
<section class="flow">
<h2>What We're About</h2>
<p>
ForwardJS Ottawa (formerly React Ottawa) is a non-profit tech meetup
group dedicated to web development technologies and community.
</p>
<p>
Founded in 2018, we now have
<span class="font-semibold">{memberCount.toLocaleString()} members</span
>
and host monthly events that bring developers together to learn, share, and
connect.
</p>
<p>We host two types of events:</p>
<ul>
<li>Traditional meetups with guest speakers and tech talks.</li>
<li>Casual socials focused on community building.</li>
</ul>

<h3>Join Us</h3>
<p>
Whether you're just getting started or you're deep into your dev career,
we'd love to have you join us. <br />

<a
href="https://www.meetup.com/ottawa-forwardjs-meetup/"
target="_blank"
class="font-semibold with-icon"
>Become a member <svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-external-link-icon lucide-external-link"
><path d="M15 3h6v6"></path><path d="M10 14 21 3"></path><path
d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"
></path></svg
></a
> and get notified about upcoming events!
</p>
</section>

<section class="flow">
<h2>Meet Our Organizers</h2>

<div class="organizers">
{
organizers.map((organizer) => {
return (
<div class="organizer">
<div class="stack-lg">
<picture class="organizer-picture">
<img
class="organizer-image"
src={organizer.imageUrl}
alt={organizer.altText}
loading="lazy"
/>
</picture>
<div class="stack-xs">
<h3>
{organizer.firstName} {organizer.lastName}
</h3>
<p class="organizer-role font-semibold">{organizer.role}</p>
<p>Joined {organizer.joined}</p>
</div>
</div>
</div>
);
})
}
</div>
</section>
</div>

<style>
.container {
margin-inline: auto;
padding-inline: 1.5rem;
max-width: 1000px;
}

.container > * {
margin-block-end: 5rem;
}

.container > *:last-child {
margin-block-end: 3rem;
}

.flow > * + * {
margin-block-start: 1rem;
}

.flow > h2 + * {
margin-block-start: 3rem;
}

.flow > * + h3 {
margin-block-start: 2.25rem;
}

.organizers {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}

.organizer {
flex: 1 1 300px;
background-color: #fff;
border-radius: 0.5rem;
padding: 3rem;
box-shadow: rgba(0, 0, 0, 0.04) 0px 3px 5px;
}

@media (min-width: 1000px) {
.organizer {
max-width: 307px;
}
}

.organizer-picture {
display: flex;
}

.organizer-image {
margin-inline: auto;
aspect-ratio: 1 / 1;
object-fit: cover;
border-radius: 50%;
display: block;
max-width: 150px;
max-height: 150px;
}

.stack-lg > * + * {
margin-block-start: 3rem;
}

.stack-xs > * + * {
margin-block-start: 0.25rem;
}

ul > li {
margin-left: 1rem;
padding-left: 0.25rem;
}

a[class*="with-icon"] {
display: inline-flex;
align-items: center;
gap: 0.125rem;
margin-inline-end: 0.125rem;
}

.font-semibold {
font-weight: 600;
}
</style>
</Layout>