diff --git a/src/components/events/event-card.tsx b/src/components/events/event-card.tsx
new file mode 100644
index 0000000000..f39d85e807
--- /dev/null
+++ b/src/components/events/event-card.tsx
@@ -0,0 +1,141 @@
+import type { ReactNode } from "react"
+import { clsx } from "clsx"
+
+import { CalendarIcon } from "@/app/conf/_design-system/pixelarticons/calendar-icon"
+import { PinIcon } from "@/app/conf/_design-system/pixelarticons/pin-icon"
+import { Tag } from "../../app/conf/_design-system/tag"
+
+const dateFormatter = new Intl.DateTimeFormat("en", {
+ day: "numeric",
+ month: "short",
+ year: "numeric",
+})
+
+const isoLikeDatePattern =
+ /^(\d{4}-\d{2}-\d{2}|\d{4}\/?\d{2}\/?\d{2}|\d{2}\/\d{2}\/\d{4})/
+
+function normaliseDate(value: EventCardProps["date"]) {
+ if (value instanceof Date && !Number.isNaN(value.getTime())) {
+ return value
+ }
+
+ if (typeof value === "string") {
+ const parsed = new Date(value)
+ if (!Number.isNaN(parsed.getTime())) {
+ return parsed
+ }
+ }
+
+ return undefined
+}
+
+function formatDateLabel(value: EventCardProps["date"]) {
+ const parsed = normaliseDate(value)
+
+ if (parsed) {
+ if (typeof value === "string" && !isoLikeDatePattern.test(value.trim())) {
+ return value.trim() || undefined
+ }
+
+ return dateFormatter.format(parsed)
+ }
+
+ if (typeof value === "string") {
+ const trimmed = value.trim()
+ return trimmed.length > 0 ? trimmed : undefined
+ }
+
+ return undefined
+}
+
+export interface EventCardProps {
+ href: string
+ date?: Date | string | null
+ city: ReactNode
+ name: ReactNode
+ meta?: ReactNode
+ official?: boolean
+}
+
+export function EventCard({
+ href,
+ date,
+ city,
+ name,
+ meta,
+ official,
+}: EventCardProps) {
+ const dateLabel = formatDateLabel(date)
+ const parsedDate = normaliseDate(date)
+
+ return (
+
+
+
+ {meta ? (
+ {meta}
+ ) : (
+ Official GraphQL Local
+ )}
+ {official && (
+
+
+ ★
+
+ Official
+
+ )}
+
+
+
+ {name}
+
+
+
+ {dateLabel && (
+
+
+ {parsedDate ? (
+
+ {dateLabel}
+
+ ) : (
+ {dateLabel}
+ )}
+
+ )}
+ {city && (
+
+ )}
+
+
+
+ )
+}
diff --git a/src/components/events.ts b/src/components/events/index.ts
similarity index 81%
rename from src/components/events.ts
rename to src/components/events/index.ts
index 7d0bf687b4..8befb6af03 100644
--- a/src/components/events.ts
+++ b/src/components/events/index.ts
@@ -1,11 +1,41 @@
-export const events = [
+export * from "./event-card"
+
+interface Event {
+ name: string
+ slug: string
+ location: string
+ date: string
+ coverImage?: string
+ eventLink: string
+ host: string
+ hostLink?: string
+}
+
+export const events: Event[] = [
+ {
+ name: "GraphQL Day at APIDays",
+ slug: "graphql-day-at-apidays",
+ location: "CNIT La Defense, Paris",
+ date: "2025-12-11T08:00:00+00:00",
+ eventLink: "https://graphql.day",
+ host: "APIDays & GraphQL Community",
+ hostLink: "https://apidays.co",
+ },
+ {
+ name: "GraphQLConf 2025",
+ slug: "graphql-conf-2025",
+ location: "Amsterdam",
+ date: "2025-09-08T07:00:00+00:00",
+ eventLink: "/conf/2025",
+ host: "GraphQL Foundation",
+ },
{
name: "GraphQLConf 2024",
slug: "graphql-conf-2024",
location: "San Francisco",
date: "2024-09-09T07:00:00+00:00",
eventLink: "/conf/2024",
- host: "JW Marriott San Francisco Union Square",
+ host: "GraphQL Foundation",
},
{
name: "GraphQL Bali 001",
@@ -30,7 +60,7 @@ export const events = [
hostLink: "https://www.oursky.com/",
},
{
- name: "Paris December 7",
+ name: "GraphQL Paris before APIDays",
slug: "graphql-paris-pre-apidays",
location: "Paris",
date: "2023-12-07T18:00:00+02:00",
@@ -51,7 +81,7 @@ export const events = [
hostLink: "https://developer.microsoft.com/en-us/reactor/",
},
{
- name: "Singapore November 6",
+ name: "GraphQL Singapore Meetup #6",
slug: "graphql-sigapore-6",
location: "Singapore",
date: "2023-11-15T18:00:00+08:00",
@@ -73,7 +103,7 @@ export const events = [
hostLink: "https://www.pinterest.com/",
},
{
- name: "Bangkok November 12",
+ name: "GraphQL BKK 12.0",
slug: "graphql-bangkok-12",
location: "Bangkok",
date: "2023-11-02T18:00:00+07:00",
@@ -84,7 +114,7 @@ export const events = [
hostLink: "https://sevenpeakssoftware.com/",
},
{
- name: "Seattle October",
+ name: "GraphQL Seattle #1",
slug: "graphql-seattle-01",
location: "Seattle",
date: "2023-10-26T18:00:00-07:00",
@@ -95,7 +125,7 @@ export const events = [
hostLink: "https://www.microsoft.com/",
},
{
- name: "San Francisco September",
+ name: "GraphQL SF #12",
slug: "graphql-sf-12",
location: "San Francisco",
date: "2023-09-19T18:00:00-07:00",
@@ -106,7 +136,7 @@ export const events = [
hostLink: "https://github.com/",
},
{
- name: "London September",
+ name: "GraphQL London #1",
slug: "graphql-london-01",
location: "London",
date: "2023-09-07T17:30:00+01:00",
@@ -117,7 +147,7 @@ export const events = [
hostLink: "https://neo4j.com/",
},
{
- name: "Bangkok July 11",
+ name: "GraphQL BKK 11.0",
slug: "graphql-bangkok-11",
location: "Bangkok",
date: "2023-07-31T18:00:00+07:00",
diff --git a/src/pages/community/events.mdx b/src/pages/community/events.mdx
index 347844939a..b067f311db 100644
--- a/src/pages/community/events.mdx
+++ b/src/pages/community/events.mdx
@@ -2,65 +2,19 @@
title: Events & Meetups
---
-{/* title can be removed in Nextra 4, since sidebar title will take from first h1 */}
-
# Events & Meetups
import { LocationIcon, ClockIcon } from "../../icons"
import { clsx } from "clsx"
import { useEffect } from "react"
import { useData } from "nextra/hooks"
-import { meetups } from "../../components/meetups"
-import { events } from "../../components/events"
import "leaflet/dist/leaflet.css"
+
+import { meetups } from "../../components/meetups"
+import { events, EventCard } from "../../components/events"
import pinkCircle from "./pink-circle.svg"
-import { Button } from '../../app/conf/_components/button'
+import { Button } from '../../app/conf/_design-system/button'
-export function EventCard({ href, date, city, name, meta, official }) {
- return (
-
- {date && date instanceof Date && (
-
-
{date.getDate()}
-
- {date.toLocaleString("en", {
- month: "short",
- year: "numeric",
- })}
-
-
- )}
-
-
{meta}
-
{name}{official ? <>{" "}⭐️ > : ""}
-
-
-
- {city}
-
- {date && (
-
-
- {date.toLocaleString("en", {
- hour: "numeric",
- minute: "numeric",
- })}
-
- )}
-
-
-
- )
-}
export const { pastEvents, upcomingEvents } = events.reduce(
(acc, event) => {
@@ -76,9 +30,19 @@ export const { pastEvents, upcomingEvents } = events.reduce(
{ pastEvents: [], upcomingEvents: [] },
)
+export function EventsScrollview({ children }) {
+ return (
+
+ {children}
+
+ )
+}
+
export function Events({ events }) {
+ if (events.length === 0) return null;
+
return (
-
+
{events.map(event => (
+## Past Events
-
-
- Past Events
-
-
-*/}
+
## Meetups
@@ -115,9 +74,11 @@ happy to promote your GraphQL event through the
Please contact us in the `#meetups-admin` channel on
[the community Discord channel](/community/#official-channels).
-
- Start a GraphQL Local!
-
+
+
+ Start a GraphQL Local!
+
+
export function Meetups() {
useEffect(() => {
@@ -143,16 +104,18 @@ export function Meetups() {
return (
<>
- {meetups.map(({ node }) => (
-
- ))}
+
+ {meetups.map(({ node }) => (
+
+ ))}
+
>
)
}
diff --git a/tailwind.config.ts b/tailwind.config.ts
index eeb7669d61..82107207ca 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -202,6 +202,7 @@ const config: Config = {
}),
tailwindMediaHover(),
scrollStartPlugin(),
+ scrollviewFadePlugin(),
browserPlugin,
],
darkMode: ["class", 'html[class~="dark"]'],
@@ -294,3 +295,54 @@ function scrollStartPlugin() {
)
})
}
+
+function scrollviewFadePlugin() {
+ return plugin(({ addComponents, addBase }) => {
+ addComponents({
+ ".scrollview-x-fade": {
+ position: "relative",
+ scrollTimeline: "--scroll-timeline-x inline",
+ "--fade-start-opacity": "1",
+ "--fade-end-opacity": "1",
+ maskImage: `
+ linear-gradient(to right,
+ hsl(0 0% 0% / var(--fade-start-opacity)),
+ black var(--fade-size),
+ black calc(100% - var(--fade-size)),
+ hsl(0 0% 0% / var(--fade-end-opacity))
+ )
+ `,
+ WebkitMaskImage: `
+ linear-gradient(to right,
+ hsl(0 0% 0% / var(--fade-start-opacity)),
+ black var(--fade-size),
+ black calc(100% - var(--fade-size)),
+ hsl(0 0% 0% / var(--fade-end-opacity))
+ )
+ `,
+ animation:
+ "scrollview-fade-start 10s ease-out both, scrollview-fade-end 10s ease-out both",
+ animationTimeline: "--scroll-timeline-x, --scroll-timeline-x",
+ animationRange: "0 2em, calc(100% - 2em) 100%",
+ },
+ "@keyframes scrollview-fade-start": {
+ from: { "--fade-start-opacity": "1" },
+ to: { "--fade-start-opacity": "0" },
+ },
+ "@keyframes scrollview-fade-end": {
+ from: { "--fade-end-opacity": "0" },
+ to: { "--fade-end-opacity": "1" },
+ },
+ "@property --fade-start-opacity": {
+ syntax: '"
"',
+ initialValue: "1",
+ inherits: "false",
+ },
+ "@property --fade-end-opacity": {
+ syntax: '""',
+ initialValue: "1",
+ inherits: "false",
+ },
+ })
+ })
+}