Skip to content

Commit f5fa0bc

Browse files
authored
Merge pull request #25 from kyoto-tech/calendar-display
updating calendar display
2 parents bf1e411 + f1feb6e commit f5fa0bc

File tree

8 files changed

+219
-86
lines changed

8 files changed

+219
-86
lines changed

package-lock.json

Lines changed: 1 addition & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@
2626
"react": "^19.2.0",
2727
"react-dom": "^19.2.0",
2828
"react-icons": "^5.5.0",
29-
"tailwindcss": "^4.1.17",
30-
"xml-js": "^1.6.11"
29+
"tailwindcss": "^4.1.17"
3130
},
3231
"devDependencies": {
3332
"@eslint/js": "^9.39.1",

src/components/Calendar.astro

Lines changed: 87 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,96 @@
11
---
2-
import EventList from '../components/EventList.jsx';
3-
const RSS_URL = "https://www.meetup.com/Kyoto-Tech-Meetup/events/rss/";
4-
5-
const rssResponse = await fetch(RSS_URL);
6-
const rssText = await rssResponse.text();
7-
8-
// Minimal XML → JS parser
9-
import { xml2js } from "xml-js";
10-
const parsed = xml2js(rssText, { compact: true });
11-
let eventNames = [];
12-
13-
if (Array.isArray(parsed.rss.channel.item)) {
14-
15-
const items = parsed.rss.channel.item.map(evt => ({
16-
title: evt.title._cdata,
17-
link: evt.link._text,
18-
pubDate: evt.pubDate._text,
19-
description: evt.description?._text ?? "",
20-
}));
21-
22-
23-
eventNames = items;
24-
25-
} else {
26-
// handle the case where it's a single item object
27-
eventNames= [];
2+
import EventList from "../components/EventList.jsx";
3+
4+
const EVENTS_URL = "https://www.meetup.com/kyoto-tech-meetup/events/";
5+
6+
async function fetchMeetupEvents() {
7+
const html = await fetch(EVENTS_URL).then((r) => r.text());
8+
9+
// Grab the JSON embedded in the __NEXT_DATA__ script tag
10+
const match = html.match(
11+
/<script id="__NEXT_DATA__" type="application\/json">([\s\S]*?)<\/script>/
12+
);
13+
14+
if (!match?.[1]) return [];
15+
16+
const data = JSON.parse(match[1]);
17+
const apolloState = data?.props?.pageProps?.__APOLLO_STATE__ as
18+
| Record<string, any>
19+
| undefined;
20+
if (!apolloState) return [];
21+
22+
const now = new Date();
23+
const cutoff = new Date();
24+
cutoff.setDate(cutoff.getDate() + 60);
25+
26+
const resolvePhotoUrl = (photoLike: any) => {
27+
const ref =
28+
(typeof photoLike === "string" && photoLike.startsWith("PhotoInfo:"))
29+
? photoLike
30+
: photoLike?.__ref ??
31+
(photoLike?.id ? `PhotoInfo:${photoLike.id}` : null);
32+
33+
if (!ref) return null;
34+
const photo = apolloState[ref];
35+
if (!photo) return null;
36+
37+
return (
38+
photo.highResUrl ??
39+
photo.source ??
40+
(photo.baseUrl && photo.id ? `${photo.baseUrl}${photo.id}` : null)
41+
);
42+
};
43+
44+
const resolveVenue = (venueLike: any) => {
45+
const ref =
46+
(typeof venueLike === "string" && venueLike.startsWith("Venue:"))
47+
? venueLike
48+
: venueLike?.__ref ??
49+
(venueLike?.id ? `Venue:${venueLike.id}` : null);
50+
51+
const venue = ref ? apolloState[ref] : venueLike;
52+
if (!venue) return null;
53+
54+
return {
55+
name: venue.name,
56+
address: venue.address,
57+
city: venue.city,
58+
state: venue.state,
59+
country: venue.country
60+
};
61+
};
62+
63+
const events = Object.entries(apolloState)
64+
.filter(([key, value]) => key.startsWith("Event:") && (value as any)?.dateTime)
65+
.map(([, rawValue]) => {
66+
const value = rawValue as any;
67+
return {
68+
title: value.title?.replace(" | Kyoto Tech Meetup", "") ?? value.title,
69+
link: value.eventUrl,
70+
start: value.dateTime,
71+
endTime: value.endTime ?? null,
72+
description: value.description ?? "",
73+
image: resolvePhotoUrl(value.featuredEventPhoto),
74+
goingCount: value.going?.totalCount ?? 0,
75+
interestedCount: value.socialProofInsights?.totalInterestedUsers ?? 0,
76+
venue: resolveVenue(value.venue)
77+
};
78+
})
79+
.filter((evt) => {
80+
const start = new Date(evt.start);
81+
return start >= now && start <= cutoff;
82+
})
83+
.sort((a, b) => new Date(a.start).valueOf() - new Date(b.start).valueOf());
84+
85+
return events;
2886
}
2987
30-
console.log("hello" + eventNames);
88+
const events = await fetchMeetupEvents();
3189
3290
---
3391

34-
<section class="p-6">
92+
<section class="pt-6 pb-6">
3593

36-
<EventList client:load events={eventNames} />
94+
<EventList client:load events={events} />
3795

3896
</section>
39-

src/components/EventList.jsx

Lines changed: 110 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,117 @@ export default function EventList({ events }) {
33
return <p className=" mt3 text-gray-500 italic">Check back soon for events! <a href="https://www.meetup.com/ja-JP/kyoto-tech-meetup"> Join our meetup group here to get updates.</a></p>;
44
}
55

6+
// Group by month string (using Tokyo timezone for consistency)
7+
const groups = events.reduce((acc, evt) => {
8+
const d = new Date(evt.start);
9+
const monthLabel = d.toLocaleString("en-US", {
10+
timeZone: "Asia/Tokyo",
11+
month: "long",
12+
year: "numeric"
13+
});
14+
acc[monthLabel] = acc[monthLabel] || [];
15+
acc[monthLabel].push(evt);
16+
return acc;
17+
}, {});
18+
19+
const orderedMonths = events
20+
.map(evt => {
21+
const d = new Date(evt.start);
22+
const monthLabel = d.toLocaleString("en-US", {
23+
timeZone: "Asia/Tokyo",
24+
month: "long",
25+
year: "numeric"
26+
});
27+
return { monthLabel, date: d };
28+
})
29+
.reduce((seen, curr) => {
30+
if (!seen.find(item => item.monthLabel === curr.monthLabel)) {
31+
seen.push(curr);
32+
}
33+
return seen;
34+
}, [])
35+
.sort((a, b) => a.date - b.date)
36+
.map(item => item.monthLabel);
37+
638
return (
7-
<ul className="space-y-6">
8-
{events.map(event => (
9-
<li key={event.link} className="p-4 border rounded-xl shadow-sm">
10-
<a
11-
href={event.link}
12-
target="_blank"
13-
rel="noopener noreferrer"
14-
className="text-xl font-semibold text-blue-600 hover:underline"
15-
>
16-
{event.title}
17-
</a>
18-
<p className="text-sm text-gray-500 mt-1">
19-
{new Date(event.pubDate).toLocaleString()}
20-
</p>
21-
<div
22-
className="prose prose-sm mt-2"
23-
dangerouslySetInnerHTML={{ __html: event.description }}
24-
/>
25-
</li>
39+
<div className="space-y-8">
40+
{orderedMonths.map(monthLabel => (
41+
<div key={monthLabel} className="space-y-4">
42+
<h3 className="text-2xl font-semibold text-slate-900">{monthLabel}</h3>
43+
<ul className="grid grid-cols-1 md:grid-cols-2 gap-6">
44+
{groups[monthLabel].map(event => (
45+
<li key={event.link}>
46+
<a
47+
href={event.link}
48+
target="_blank"
49+
rel="noopener noreferrer"
50+
className="p-4 border rounded-xl shadow-sm flex gap-4 items-start md:items-center block hover:shadow-md transition-shadow no-underline h-full"
51+
style={{
52+
color: "var(--accent)",
53+
textDecoration: "none",
54+
borderColor: "var(--accent)"
55+
}}
56+
>
57+
{event.image ? (
58+
<div className="w-1/3 min-w-[120px]">
59+
<img
60+
src={event.image}
61+
alt={event.title}
62+
className="w-full h-full max-h-32 rounded-lg object-cover"
63+
loading="lazy"
64+
/>
65+
</div>
66+
) : null}
67+
<div className="flex-1 min-w-0">
68+
<div className="text-xl font-semibold">
69+
{event.title}
70+
</div>
71+
<p className="text-sm text-gray-500 mt-1 space-x-2">
72+
<span>
73+
{new Date(event.start).toLocaleString("en-US", {
74+
timeZone: "Asia/Tokyo",
75+
month: "long",
76+
day: "numeric",
77+
year: "numeric"
78+
})}
79+
</span>
80+
<span className="text-gray-400"></span>
81+
<span>
82+
{new Date(event.start).toLocaleTimeString("en-US", {
83+
timeZone: "Asia/Tokyo",
84+
hour12: false,
85+
hour: "2-digit",
86+
minute: "2-digit"
87+
})}
88+
{event.endTime
89+
? ` – ${new Date(event.endTime).toLocaleTimeString("en-US", {
90+
timeZone: "Asia/Tokyo",
91+
hour12: false,
92+
hour: "2-digit",
93+
minute: "2-digit"
94+
})}`
95+
: ""}
96+
</span>
97+
</p>
98+
<div className="text-sm text-gray-700 mt-2 space-y-1">
99+
<div className="font-medium text-slate-900">
100+
{event.venue?.name ?? "Venue TBA"}
101+
</div>
102+
{event.venue?.address ? (
103+
<div className="text-gray-600">{event.venue.address}</div>
104+
) : null}
105+
</div>
106+
<div className="flex flex-wrap items-center gap-3 mt-3 text-sm text-gray-600">
107+
<span>{event.goingCount ?? 0} going</span>
108+
<span>· {event.interestedCount ?? 0} interested</span>
109+
</div>
110+
</div>
111+
</a>
112+
</li>
113+
))}
114+
</ul>
115+
</div>
26116
))}
27-
</ul>
117+
</div>
28118
);
29119
}

src/components/LanguagePicker.astro

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
---
2-
import { languages } from '../i18n/ui';
3-
import { getRelativeLocaleUrl } from 'astro:i18n';
2+
import { languages } from "../i18n/ui";
3+
import { getRelativeLocaleUrl } from "astro:i18n";
44
55
const langsToFlags = {
66
en: "GB.svg",
77
ja: "JP.svg"
8-
};
8+
} as const;
99
10-
const activeLang = Astro.currentLocale;
11-
const allLangs = Object.entries(languages);
10+
const activeLang = Astro.currentLocale as "en" | "ja" | undefined;
11+
const allLangs = Object.entries(languages) as [keyof typeof languages, string][];
1212
1313
---
1414
<ul class="absolute top-4 right-4 md:top-6 md:right-6 z-20 flex gap-2">
1515
{allLangs.map(([lang, label]) => {
1616
const isActive = lang === activeLang;
17+
const flag = langsToFlags[lang] ?? "";
1718

1819
return (
1920
<li>
@@ -26,7 +27,7 @@ const allLangs = Object.entries(languages);
2627
>
2728
<span class="sr-only">{label}</span>
2829
<img
29-
src={`/flags/${langsToFlags[lang]}`}
30+
src={`/flags/${flag}`}
3031
alt=""
3132
aria-hidden="true"
3233
width="24"

src/layouts/Layout.astro

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
---
22
import "../styles/global.css";
3-
import { useTranslations } from '../i18n/utils';
4-
import { getRelativeLocaleUrl } from 'astro:i18n';
3+
import { useTranslations } from "../i18n/utils";
4+
import { getRelativeLocaleUrl } from "astro:i18n";
55
6-
const activeLang = Astro.currentLocale;
7-
const t = useTranslations(activeLang || 'en');
6+
const activeLang = (Astro.currentLocale ?? "en") as "en" | "ja";
7+
const t = useTranslations(activeLang);
88
---
99

1010
<!doctype html>
@@ -25,7 +25,7 @@ const t = useTranslations(activeLang || 'en');
2525
property="og:description"
2626
content={t("meta.description")}
2727
/>
28-
<meta property="og:url" content=`https://kyoto-tech.github.io${getRelativeLocaleUrl(activeLang, "/")}` />
28+
<meta property="og:url" content={`https://kyoto-tech.github.io${getRelativeLocaleUrl(activeLang, "/")}`} />
2929
<meta property="og:image" content="https://kyoto-tech.github.io/og/kyoto-tech-meetup.png" />
3030
<meta property="og:image:width" content="1200" />
3131
<meta property="og:image:height" content="630" />

src/pages/index.astro

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,16 @@ import Layout from "../layouts/Layout.astro";
33
import WhyJoin from "../components/WhyJoin.astro";
44
import WhatWeDo from "../components/WhatWeDo.astro";
55
import Calendar from "../components/Calendar.astro";
6-
import EventList from "../components/EventList.jsx";
76
import { FaGithub, FaDiscord, FaMeetup } from "react-icons/fa";
8-
import { useTranslations } from '../i18n/utils';
9-
import LanguagePicker from '../components/LanguagePicker.astro';
7+
import { useTranslations } from "../i18n/utils";
8+
import LanguagePicker from "../components/LanguagePicker.astro";
109
11-
const { lang = 'en' } = Astro.props;
10+
const { lang = "en" } = Astro.props;
1211
const t = useTranslations(lang);
1312
14-
const meetupUrl = lang === 'ja'
15-
? 'https://www.meetup.com/ja-JP/kyoto-tech-meetup/'
16-
: 'https://www.meetup.com/kyoto-tech-meetup/';
13+
const meetupUrl = lang === "ja"
14+
? "https://www.meetup.com/ja-JP/kyoto-tech-meetup/"
15+
: "https://www.meetup.com/kyoto-tech-meetup/";
1716
1817
type ActivityContent =
1918
| {

src/pages/ja/index.astro

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
---
2-
import RootIndex from '../index.astro';
2+
import RootIndex from "../index.astro";
33
---
4-
<RootIndex lang="ja" />
4+
<RootIndex lang="ja" />

0 commit comments

Comments
 (0)