Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
47 changes: 38 additions & 9 deletions src/app/(main)/community/events/event-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ export interface EventCardProps {
city: ReactNode
name: ReactNode
meta?: ReactNode
kind: "meetup" | "conference" | "working-group" | "foundation-meeting"
kind: EventKind
compact?: boolean
className?: string
}

Expand All @@ -65,6 +66,7 @@ const classNameFromKind = {
meetup: "bg-[hsl(var(--color-sec-base)/var(--bg-opacity))]",
"working-group": "bg-[hsl(229deg_100%_70.4%_/_var(--bg-opacity))]",
"foundation-meeting": "bg-[hsl(35deg_100%_63%_/_var(--bg-opacity))]",
workshop: "bg-[hsl(152deg_53%_48%_/_var(--bg-opacity))]",
} satisfies { [kind in EventKind]: string }

export function EventCard({
Expand All @@ -74,6 +76,7 @@ export function EventCard({
name,
meta,
kind,
compact,
className,
}: EventCardProps) {
const dateLabel = formatDateLabel(date)
Expand All @@ -82,26 +85,44 @@ export function EventCard({
<a
href={href}
className={clsx(
"gql-focus-visible group flex min-h-[214px] min-w-[260px] flex-col overflow-hidden border border-neu-200 text-left text-current no-underline ring-neu-400 hover:ring-1 hover:ring-offset-1 hover:ring-offset-neu-0 dark:border-neu-50 dark:ring-neu-100 xs:min-w-[352px] lg:w-[408px]",
"gql-focus-visible group flex min-w-[260px] flex-col overflow-hidden border border-neu-200 text-left text-current no-underline ring-neu-400 hover:ring-1 hover:ring-offset-1 hover:ring-offset-neu-0 dark:border-neu-50 dark:ring-neu-100 xs:min-w-[352px] lg:w-[408px]",
"[--bg-opacity:0.05] hover:[--bg-opacity:0.07] dark:[--bg-opacity:0.03] hover:dark:[--bg-opacity:0.06]",
"z-[4]",
compact ? "xs:min-w-0 lg:w-full" : "min-h-[214px]",
classNameFromKind[kind],
className,
)}
target="_blank"
rel="noreferrer"
>
<div className="flex flex-1 flex-col">
<div
className={clsx(
"flex flex-1 flex-col",
compact && "md:flex-row md:items-center",
)}
>
<div
className={clsx(
"flex h-[45px] items-center justify-between gap-2 px-2 text-neu-800 dark:text-neu-600 xs:px-4",
meta
? "border-b border-neu-200 py-2.5 dark:border-neu-50"
: "-mb-2 pt-2 xs:-mb-4 xs:pt-3",
"flex items-center justify-between gap-2 px-2 text-neu-800 dark:text-neu-600 xs:px-4",
compact
? "pt-2 md:order-2 md:min-w-0 md:flex-1 md:justify-start md:gap-3 md:py-2"
: "h-[45px]",
!compact &&
(meta
? "border-b border-neu-200 py-2.5 dark:border-neu-50"
: "-mb-2 pt-2 xs:-mb-4 xs:pt-3"),
)}
>
<Tag color={eventTagColors[kind]}>{kind.replace("-", " ")}</Tag>
{!!meta && (
{!!meta && compact && (
<Tag
color="hsl(var(--color-neu-500))"
className="normal-case max-md:hidden"
>
Hosted by: {meta}
</Tag>
)}
{!!meta && !compact && (
<span
className={clsx(
String(meta).length < 22
Expand All @@ -115,13 +136,21 @@ export function EventCard({
)}
</div>

<div className="typography-h3 flex min-h-[100px] flex-1 flex-col justify-center px-2 py-3 text-neu-900 xs:min-h-[124px] xs:px-4 xs:py-6">
<div
className={clsx(
"flex flex-1 flex-col justify-center px-2 text-neu-900 xs:px-4",
compact
? "typography-body-lg py-1 font-medium md:order-1 md:max-w-[45%] md:flex-none md:py-2"
: "typography-h3 min-h-[100px] py-3 xs:min-h-[124px] xs:py-6",
)}
>
{name}
</div>

<div
className={clsx(
"flex flex-wrap border-t border-neu-200 text-neu-800 dark:border-neu-50",
compact && "md:order-3 md:w-[26rem] md:shrink-0 md:border-t-0",
dateLabel && city
? "grid grid-cols-2 divide-x divide-neu-200 dark:divide-neu-50"
: "",
Expand Down
2 changes: 2 additions & 0 deletions src/app/(main)/community/events/event-filter-tag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ export type EventKind =
| "conference"
| "working-group"
| "foundation-meeting"
| "workshop"

export const eventTagColors = {
conference: "hsl(var(--color-pri-base))",
meetup: "hsl(var(--color-sec-dark))",
"working-group": "#6883FF",
"foundation-meeting": "#FFB140",
workshop: "#3DBE7E",
} satisfies { [kind in EventKind]: string }

export interface EventFilterTagProps
Expand Down
54 changes: 26 additions & 28 deletions src/app/(main)/community/events/events-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import { clsx } from "clsx"
import type { WorkingGroupMeeting as CalendarEvent } from "@/../scripts/sync-working-groups/sync-working-groups"

import { EventCard } from "./event-card"
import { EventsScrollview } from "./events-scrollview"
import type { Event } from "./events"
import { EventFilterTag, EventKind } from "./event-filter-tag"

const PAGE_SIZE = 12

interface FilterChipProps extends ComponentPropsWithoutRef<"button"> {
active?: boolean
count?: number
Expand Down Expand Up @@ -63,8 +64,11 @@ const DEFAULT_VISIBILITY = {
meetup: true,
"working-group": true,
"foundation-meeting": true,
workshop: true,
} satisfies Record<EventKind, boolean>

const tags = Object.keys(DEFAULT_VISIBILITY) as EventKind[]

const DAY_IN_MS = 24 * 60 * 60 * 1000

// We want to show at least one of each WG, a month might be 31 days long and
Expand Down Expand Up @@ -165,29 +169,19 @@ export function EventsList({
children?: React.ReactNode
}) {
const [kindFilters, setKindFilters] = useState(DEFAULT_VISIBILITY)
const [limit, setLimit] = useState(PAGE_SIZE)

const { events, tags } = useMemo(() => {
const visibleTags: Set<EventKind> = new Set()
/** Determines if this event kind should be shown, and if so ensures the tag is present in tags */
const events = useMemo(() => {
/** Determines if this event kind should be shown */
const shouldShow = (kind: EventKind, date?: Date): boolean => {
if (kind === "working-group" || kind === "foundation-meeting") {
if (!date) return false
// we filter out all working groups further in the future than 30 days
if (!isSoon(date)) return false
}
visibleTags.add(kind)
return kindFilters[kind]
}
const majorEvents: AnyEvent[] = []
const minorEvents: AnyEvent[] = []

// Prioritize major vs minor events
const target = {
conference: majorEvents,
meetup: majorEvents,
"working-group": minorEvents,
"foundation-meeting": minorEvents,
} satisfies { [kind in EventKind]: AnyEvent[] }
const events: AnyEvent[] = []

for (const event of allEvents) {
const kind = categorizeEvent(event)
Expand All @@ -203,22 +197,15 @@ export function EventsList({
} else if ("start" in event) {
const date = new Date(event.start)
if (shouldShow(kind, date)) {
target[kind].push(event)
events.push(event)
}
} else {
if (shouldShow(kind)) {
target[kind].push(event)
events.push(event)
}
}
}
// Ensure that major events are surfaced higher than minor events, even though this breaks date order
const events = [...majorEvents, ...minorEvents]
const keys = Object.keys(DEFAULT_VISIBILITY)
// Sort tags into a sensible order
const tags = [...visibleTags].sort(
(a, z) => keys.indexOf(a) - keys.indexOf(z),
)
return { events, tags }
return events
}, [allEvents, kindFilters])

return (
Expand Down Expand Up @@ -248,8 +235,8 @@ export function EventsList({
</fieldset>
{children}
</div>
<EventsScrollview>
{events.map(event => {
<div className="flex flex-col gap-2">
{events.slice(0, limit).map(event => {
const kind = categorizeEvent(event)
if (kind === "duplicate" || kind == null) {
return null
Expand All @@ -263,6 +250,7 @@ export function EventsList({
name={calendarEventName(event)}
city={calendarEventCity(event)}
kind={kind}
compact
/>
) : (
<EventCard
Expand All @@ -273,10 +261,20 @@ export function EventsList({
name={event.name}
city={event.location}
kind={kind}
compact
/>
)
})}
</EventsScrollview>
{events.length > limit && (
<button
type="button"
onClick={() => setLimit(limit + PAGE_SIZE)}
className="gql-focus-visible typography-body-md border border-neu-200 py-3 text-neu-800 hover:bg-neu-100 dark:border-neu-50 dark:text-neu-600 dark:hover:bg-neu-50"
>
Load more
</button>
)}
</div>
</div>
)
}
38 changes: 24 additions & 14 deletions src/app/(main)/community/events/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,48 +34,58 @@ const allEvents: Event[] = [
kind: "meetup",
},
{
name: "GraphQL Day at FOST London",
name: "From Zero to Agentic Query in One Afternoon",
slug: "jonthebeach-workshop-2026",
location: "Málaga, Spain",
date: "2026-10-29T09:00:00+02:00",
eventLink: "https://jonthebeach.com/workshops/from-zero-to-agentic/",
host: "J On The Beach",
hostLink: "https://jonthebeach.com",
kind: "workshop" as const,
},
{
name: "GraphQL Day London",
slug: "graphql-day-fost-london-2026",
location: "London",
date: "2026-10-01T09:00:00+01:00",
eventLink: "/day/2026/london",
host: "FOST & GraphQL Community",
host: "FOST London",
hostLink: "https://www.joinfost.io",
},
{
name: "GraphQL Days at FOST Paris",
name: "GraphQL Days Paris",
slug: "graphql-day-fost-paris-2026",
location: "Paris",
date: "2026-12-01T09:00:00+01:00",
eventLink: "/day/2026/paris",
host: "FOST & GraphQL Community",
host: "FOST Paris",
hostLink: "https://www.joinfost.io",
},
{
name: "GraphQL Day at FOST Melbourne",
name: "GraphQL Day Melbourne",
slug: "graphql-day-fost-melbourne-2026",
location: "Melbourne",
date: "2026-10-28T09:00:00+11:00",
eventLink: "/day/2026/melbourne",
host: "FOST & GraphQL Community",
host: "FOST Melbourne",
hostLink: "https://www.joinfost.io",
},
{
name: "GraphQL Day at FOST Bengaluru",
name: "GraphQL Day Bengaluru",
slug: "graphql-day-fost-bengaluru-2026",
location: "Bengaluru",
date: "2026-08-19T09:00:00+05:30",
eventLink: "/day/2026/bengaluru",
host: "FOST & GraphQL Community",
host: "FOST Bengaluru",
hostLink: "https://www.joinfost.io",
},
{
name: "GraphQL Day at FOST Amsterdam",
name: "GraphQL Day Amsterdam",
slug: "graphql-day-fost-amsterdam-2026",
location: "Amsterdam",
date: "2026-06-09T09:00:00+01:00",
eventLink: "/day/2026/amsterdam",
host: "FOST & GraphQL Community",
host: "FOST Amsterdam",
hostLink: "https://www.joinfost.io",
},
{
Expand All @@ -87,12 +97,12 @@ const allEvents: Event[] = [
host: "GraphQL Foundation",
},
{
name: "GraphQL Day at FOST NYC",
name: "GraphQL Day NYC",
slug: "graphql-day-fost-nyc-2026",
location: "New York",
date: "2026-05-13T09:00:00-04:00",
eventLink: "/day/2026/nyc",
host: "FOST & GraphQL Community",
host: "FOST NYC",
hostLink: "https://www.joinfost.io",
},
{
Expand All @@ -104,12 +114,12 @@ const allEvents: Event[] = [
host: "GraphQL Foundation",
},
{
name: "GraphQL Day at FOST Singapore",
name: "GraphQL Day Singapore",
slug: "graphql-day-fost-singapore-2026",
location: "Singapore",
date: "2026-04-14T09:00:00+08:00",
eventLink: "/day/2026/singapore",
host: "FOST & GraphQL Community",
host: "FOST Singapore",
hostLink: "https://www.joinfost.io",
},
{
Expand Down
6 changes: 3 additions & 3 deletions src/app/(main)/community/events/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export default async function EventsPage() {
sections={[
"Upcoming events",
"Meetups",
{ name: "Past events & meetups", href: "#past-events-and-meetups" },
{ name: "Past events", href: "#past-events" },
"Events gallery",
"Benefits of getting involved",
"Get your meetup noticed",
Expand Down Expand Up @@ -104,8 +104,8 @@ export default async function EventsPage() {
<MeetupsMap />
</section>

<section className="gql-section" id="past-events-and-meetups">
<h2 className="typography-h2">Past events & meetups</h2>
<section className="gql-section" id="past-events">
<h2 className="typography-h2">Past events</h2>
<p className="typography-body-md my-6 lg:mb-12">
A look back at how the GraphQL community connects and grows
together.
Expand Down
Loading