Skip to content
Merged
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
8 changes: 7 additions & 1 deletion docs/homepage-redesign-roadmap.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Homepage Redesign Roadmap

Status: Active — PRs 1–4 and 4A merged; Community links and page order in progress
Status: Active — PRs 1–5 and 4A merged; responsive events in progress
Primary audience: People considering their first Kyoto Tech Meetup
Secondary audience: Existing community members looking for events, venue details, conversations, and member work

Expand Down Expand Up @@ -394,6 +394,12 @@ Preserve the detailed calendar for existing members while giving mobile visitors
- Render the list at mobile widths and retain the existing server-rendered calendar grid at tablet and desktop widths.
- Preserve a useful no-events state linking to the Meetup group.

### Event details consistency

- Add the cached RSVP count to the hero's next-event card.
- Add the same validated “Open in Maps” action used by calendar events to the hero card when a physical address is available.
- Keep RSVP counts and Maps behavior consistent across the hero, mobile list, and desktop calendar.

### Happening-now state

- Preserve the existing `NextEventCard.astro` behavior, which already renders “Live Now” / “開催中” with an active green status dot when `getHeroEventState` identifies an ongoing event.
Expand Down
21 changes: 20 additions & 1 deletion src/components/Calendar.astro
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@ import {
type MeetupEvent,
} from "../lib/meetup-events";
import { buildFiveWeekCalendar } from "../lib/calendar-grid";
import { isOngoingEvent } from "../lib/event-status";
import { useTranslations } from "../i18n/utils";
import { FaLocationDot, FaUserGroup } from "react-icons/fa6";

type Props = {
events: MeetupEvent[];
lang?: "en" | "ja";
now?: Date;
};

const { events, lang = "en" } = Astro.props as Props;
const { events, lang = "en", now = new Date() } = Astro.props as Props;
const t = useTranslations(lang);
const locale = lang === "ja" ? "ja-JP" : "en-US";
const weeks = buildFiveWeekCalendar(events);
Expand Down Expand Up @@ -90,14 +92,31 @@ const eventColorClass = (event: MeetupEvent) => {
const mapsUrl = buildMeetupVenueMapsUrl(event.venue);
const venueLabel =
event.venue?.name ?? event.venue?.address ?? "";
const ongoing = isOngoingEvent(event, now);

return (
<article
class:list={[
"rounded-lg px-2 py-2 text-xs text-white shadow-sm transition",
eventColorClass(event),
]}
data-event-live
data-event-start={event.start}
data-event-end={event.endTime ?? undefined}
data-live-state={ongoing ? "ongoing" : "upcoming"}
data-live-emphasis="calendar"
>
<span
class="mb-1.5 inline-flex items-center gap-1 rounded-full bg-white/95 px-1.5 py-0.5 text-[0.625rem] font-bold uppercase tracking-wide text-emerald-700"
data-live-only
hidden={!ongoing}
>
<span
class="h-1.5 w-1.5 rounded-full bg-emerald-500"
aria-hidden="true"
/>
{t("home.eventCard.liveNow")}
</span>
<a
href={event.link}
class="block font-semibold leading-snug underline-offset-2 hover:underline focus-visible:rounded-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
Expand Down
29 changes: 29 additions & 0 deletions src/components/LiveEventUpdater.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script>
import { isEventTimeOngoing } from "../lib/event-status";

const refreshLiveEventStates = () => {
document.querySelectorAll<HTMLElement>("[data-event-live]").forEach((root) => {
const start = root.dataset.eventStart;
if (!start) return;

const ongoing = isEventTimeOngoing(
start,
root.dataset.eventEnd || null,
new Date(),
);
root.dataset.liveState = ongoing ? "ongoing" : "upcoming";

root.querySelectorAll<HTMLElement>("[data-live-only]").forEach((element) => {
element.hidden = !ongoing;
});
root
.querySelectorAll<HTMLElement>("[data-upcoming-only]")
.forEach((element) => {
element.hidden = ongoing;
});
});
};

refreshLiveEventStates();
window.setInterval(refreshLiveEventStates, 30_000);
</script>
70 changes: 61 additions & 9 deletions src/components/NextEventCard.astro
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
---
import type { MeetupEvent } from "../lib/meetup-events";
import {
buildMeetupVenueMapsUrl,
type MeetupEvent,
} from "../lib/meetup-events";
import { getHeroEventState } from "../lib/hero-event";
import { useTranslations } from "../i18n/utils";
import { FaLocationDot, FaUserGroup } from "react-icons/fa6";

type Props = {
event: MeetupEvent | null;
Expand All @@ -18,6 +22,8 @@ const {
} = Astro.props as Props;
const t = useTranslations(lang);
const state = getHeroEventState(event, { fallbackUrl, lang, now });
const mapsUrl = event ? buildMeetupVenueMapsUrl(event.venue) : null;
const venueLabel = event?.venue?.name ?? event?.venue?.address ?? "";
---

<section
Expand All @@ -27,17 +33,24 @@ const state = getHeroEventState(event, { fallbackUrl, lang, now });
{
state.kind === "event" ? (
<>
<div class="flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.16em] text-slate-500">
<div
class="flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.16em] text-slate-500"
data-event-live
data-event-start={event?.start}
data-event-end={event?.endTime ?? undefined}
data-live-state={state.isOngoing ? "ongoing" : "upcoming"}
>
<span
class:list={[
"h-2 w-2 rounded-full",
state.isOngoing ? "bg-emerald-500" : "bg-(--accent)",
]}
class="h-2 w-2 rounded-full"
data-live-dot
aria-hidden="true"
/>
{state.isOngoing
? t("home.eventCard.liveNow")
: t("home.eventCard.next")}
<span data-live-only hidden={!state.isOngoing}>
{t("home.eventCard.liveNow")}
</span>
<span data-upcoming-only hidden={state.isOngoing}>
{t("home.eventCard.next")}
</span>
</div>
<h2 class="mt-3 text-xl font-semibold leading-snug text-slate-950 md:text-2xl">
{state.title}
Expand All @@ -62,6 +75,45 @@ const state = getHeroEventState(event, { fallbackUrl, lang, now });
<dt class="sr-only">{t("home.eventCard.venue")}</dt>
<dd>{state.venueName ?? t("home.eventCard.venueTba")}</dd>
</div>
<div
class="flex items-start gap-2.5"
aria-label={t("home.calendar.rsvpCount").replace(
"{count}",
String(event?.goingCount ?? 0),
)}
>
<FaUserGroup
className="mt-0.5 h-4 w-4 shrink-0"
aria-hidden="true"
/>
<dt class="sr-only">{t("home.eventCard.going")}</dt>
<dd aria-hidden="true">{event?.goingCount ?? 0}</dd>
</div>
{
mapsUrl ? (
<div class="flex items-start gap-2.5">
<FaLocationDot
className="mt-0.5 h-4 w-4 shrink-0"
aria-hidden="true"
/>
<dt class="sr-only">{t("home.calendar.openInMaps")}</dt>
<dd>
<a
href={mapsUrl}
target="_blank"
rel="noreferrer"
aria-label={t("home.calendar.openVenueInMaps").replace(
"{venue}",
venueLabel,
)}
class="font-medium text-slate-700 underline decoration-slate-300 underline-offset-2 transition hover:text-slate-950 hover:decoration-(--accent) focus-visible:rounded-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-700"
>
{t("home.calendar.openInMaps")}
</a>
</dd>
</div>
) : null
}
</dl>
<a
href={state.href}
Expand Down
190 changes: 190 additions & 0 deletions src/components/UpcomingEventList.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
---
import {
buildMeetupVenueMapsUrl,
type MeetupEvent,
} from "../lib/meetup-events";
import { isOngoingEvent } from "../lib/event-status";
import { useTranslations } from "../i18n/utils";
import {
FaArrowRight,
FaClock,
FaLocationDot,
FaUserGroup,
} from "react-icons/fa6";

type Props = {
events: MeetupEvent[];
lang?: "en" | "ja";
meetupUrl: string;
now?: Date;
};

const {
events,
lang = "en",
meetupUrl,
now = new Date(),
} = Astro.props as Props;
const t = useTranslations(lang);
const locale = lang === "ja" ? "ja-JP" : "en-US";
const visibleEvents = events.slice(0, 4);
const headingId = `upcoming-events-heading-${lang}`;

const eventTypeKeys = {
coffee: "home.calendar.legend.coffee.title",
"hack-day": "home.calendar.legend.hackDay.title",
special: "home.calendar.legend.special.title",
} as const;

const formatDate = (value: string) =>
new Intl.DateTimeFormat(locale, {
day: "numeric",
month: "short",
timeZone: "Asia/Tokyo",
weekday: "short",
}).format(new Date(value));

const formatTime = (event: MeetupEvent) => {
const formatter = new Intl.DateTimeFormat(locale, {
hour: "numeric",
hour12: lang === "en",
minute: "2-digit",
timeZone: "Asia/Tokyo",
});
const start = formatter.format(new Date(event.start));
return event.endTime
? `${start}–${formatter.format(new Date(event.endTime))}`
: start;
};
---

<section class="pt-6 md:hidden" aria-labelledby={headingId}>
<h3 id={headingId} class="text-lg font-semibold text-slate-900">
{t("home.upcomingEvents.title")}
</h3>

{
visibleEvents.length > 0 ? (
<ol class="mt-4 grid gap-4">
{visibleEvents.map((event) => {
const ongoing = isOngoingEvent(event, now);
const mapsUrl = buildMeetupVenueMapsUrl(event.venue);
const venueLabel =
event.venue?.name?.trim() ||
event.venue?.address?.trim() ||
t("home.eventCard.venueTba");

return (
<li>
<article
class="rounded-2xl border border-l-4 border-slate-200 border-l-transparent bg-white p-5 shadow-sm"
data-event-live
data-event-start={event.start}
data-event-end={event.endTime ?? undefined}
data-live-state={ongoing ? "ongoing" : "upcoming"}
data-live-emphasis="card"
>
<div class="flex flex-wrap items-center gap-2 text-xs font-semibold uppercase tracking-[0.12em]">
<span
class="inline-flex items-center gap-1.5 text-emerald-700"
data-live-only
hidden={!ongoing}
>
<span
class="h-2 w-2 rounded-full bg-emerald-500"
aria-hidden="true"
/>
{t("home.eventCard.liveNow")}
</span>
<span class="text-slate-500">
{t(eventTypeKeys[event.eventType])}
</span>
</div>

<p class="mt-3 text-sm font-semibold text-(--accent)">
<time datetime={event.start}>{formatDate(event.start)}</time>
</p>
<h4 class="mt-1 text-xl font-semibold leading-snug text-slate-950">
<a
href={event.link}
class="rounded-sm underline-offset-4 hover:text-(--accent) hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-slate-700"
>
{event.title}
</a>
</h4>

<dl class="mt-4 grid gap-2 text-sm text-slate-600">
<div class="flex items-start gap-2.5">
<FaClock
className="mt-0.5 h-4 w-4 shrink-0"
aria-hidden="true"
/>
<dt class="sr-only">{t("home.eventCard.dateTime")}</dt>
<dd>{formatTime(event)}</dd>
</div>
<div class="flex items-start gap-2.5">
<FaLocationDot
className="mt-0.5 h-4 w-4 shrink-0"
aria-hidden="true"
/>
<dt class="sr-only">{t("home.eventCard.venue")}</dt>
<dd>{venueLabel}</dd>
</div>
</dl>

<div class="mt-5 flex flex-wrap items-center gap-x-5 gap-y-3 border-t border-slate-100 pt-4 text-sm">
<span
class="inline-flex items-center gap-2 font-medium text-slate-600"
aria-label={t("home.calendar.rsvpCount").replace(
"{count}",
String(event.goingCount),
)}
>
<FaUserGroup className="h-4 w-4" aria-hidden="true" />
<span aria-hidden="true">{event.goingCount}</span>
</span>
{mapsUrl ? (
<a
href={mapsUrl}
target="_blank"
rel="noreferrer"
aria-label={t("home.calendar.openVenueInMaps").replace(
"{venue}",
venueLabel,
)}
class="inline-flex items-center gap-2 font-semibold text-slate-700 underline decoration-slate-300 underline-offset-4 hover:text-slate-950 hover:decoration-(--accent) focus-visible:rounded-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-700"
>
<FaLocationDot className="h-4 w-4" aria-hidden="true" />
{t("home.calendar.openInMaps")}
</a>
) : null}
<a
href={event.link}
class="ml-auto inline-flex min-h-11 items-center gap-2 rounded-full bg-slate-950 px-5 py-2.5 font-semibold text-white transition hover:bg-slate-800 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-slate-700"
>
{t("home.upcomingEvents.rsvp")}
<FaArrowRight className="h-3.5 w-3.5" aria-hidden="true" />
</a>
</div>
</article>
</li>
);
})}
</ol>
) : (
<div class="mt-4 rounded-2xl border border-slate-200 bg-slate-50 p-5">
<p class="font-semibold text-slate-900">{t("home.eventCard.empty")}</p>
<p class="mt-2 text-sm leading-relaxed text-slate-600">
{t("home.eventCard.emptyLink")}
</p>
<a
href={meetupUrl}
class="mt-4 inline-flex min-h-11 items-center gap-2 rounded-full bg-slate-950 px-5 py-2.5 text-sm font-semibold text-white"
>
{t("home.eventCard.groupCta")}
<FaArrowRight className="h-3.5 w-3.5" aria-hidden="true" />
</a>
</div>
)
}
</section>
Loading
Loading