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} + )} +
+ )} + {city && ( +
+ + {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). - +
+ +
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", + }, + }) + }) +}