diff --git a/src/app/conf/2023/page.tsx b/src/app/conf/2023/page.tsx index 5f58405460..c4e160cc40 100644 --- a/src/app/conf/2023/page.tsx +++ b/src/app/conf/2023/page.tsx @@ -24,7 +24,7 @@ export default function ConfPage() { function Hero() { return ( -
+
diff --git a/src/app/conf/2025/_data.ts b/src/app/conf/2025/_data.ts new file mode 100644 index 0000000000..e172f3e519 --- /dev/null +++ b/src/app/conf/2025/_data.ts @@ -0,0 +1,84 @@ +import "server-only" +import { stripHtml } from "string-strip-html" +import { SchedSpeaker, ScheduleSession } from "@/app/conf/2023/types" +import pLimit from "p-limit" + +async function fetchData(url: string): Promise { + try { + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + "User-Agent": "GraphQL Conf / GraphQL Foundation", + }, + }) + const data = await response.json() + return data + } catch (error) { + throw new Error( + `Error fetching data from ${url}: ${(error as Error).message || (error as Error).toString()}`, + ) + } +} + +const token = process.env.SCHED_ACCESS_TOKEN_2024 + +async function getUsernames(): Promise { + const response = await fetchData<{ username: string }[]>( + `https://graphqlconf2024.sched.com/api/user/list?api_key=${token}&format=json&fields=username`, + ) + return response.map(user => user.username) +} + +const limit = pLimit(40) // rate limit is 30req/min + +async function getSpeakers(): Promise { + const usernames = await getUsernames() + + const users = await Promise.all( + usernames.map(username => + limit(() => { + return fetchData( + `https://graphqlconf2024.sched.com/api/user/get?api_key=${token}&by=username&term=${username}&format=json&fields=username,company,position,name,about,location,url,avatar,role,socialurls`, + ) + }), + ), + ) + + const result = users + .filter(speaker => speaker.role.includes("speaker")) + .map(user => { + return { + ...user, + about: stripHtml(user.about).result, + } + }) + + return result +} + +async function getSchedule(): Promise { + const sessions = await fetchData( + `https://graphqlconf2024.sched.com/api/session/export?api_key=${token}&format=json`, + ) + + const result = sessions.map(session => { + const { description } = session + if (description?.includes("<")) { + // console.log(`Found HTML element in about field for session "${session.name}"`) + } + + return { + ...session, + description: description && stripHtml(description).result, + } + }) + + return result +} + +// @ts-expect-error -- fixme +export const speakers = await getSpeakers() + +// @ts-expect-error -- fixme +export const schedule = await getSchedule() diff --git a/src/app/conf/2025/_videos.ts b/src/app/conf/2025/_videos.ts new file mode 100644 index 0000000000..e8b9f74c8b --- /dev/null +++ b/src/app/conf/2025/_videos.ts @@ -0,0 +1,10 @@ +export const videos: { + id: string + title: string +}[] = [ + // temporary + { + id: "fA81OFu9BVY", + title: `Top 10 GraphQL Security Checks for Every Developer - Ankita Gupta, Ankush Jain - Akto.io`, + }, +] diff --git a/src/app/conf/2025/assets/graphql-foundation-wordmark.svg b/src/app/conf/2025/assets/graphql-foundation-wordmark.svg index 556ab3f50b..8ecff1b893 100644 --- a/src/app/conf/2025/assets/graphql-foundation-wordmark.svg +++ b/src/app/conf/2025/assets/graphql-foundation-wordmark.svg @@ -1,33 +1,113 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/conf/2025/components/become-a-sponsor/blur-blob.webp b/src/app/conf/2025/components/become-a-sponsor/blur-blob.webp new file mode 100644 index 0000000000..af6299d6bd Binary files /dev/null and b/src/app/conf/2025/components/become-a-sponsor/blur-blob.webp differ diff --git a/src/app/conf/2025/components/become-a-sponsor/index.tsx b/src/app/conf/2025/components/become-a-sponsor/index.tsx new file mode 100644 index 0000000000..f0a3964b11 --- /dev/null +++ b/src/app/conf/2025/components/become-a-sponsor/index.tsx @@ -0,0 +1,125 @@ +import clsx from "clsx" + +import { Button } from "../../../_design-system/button" + +import blurBlob from "./blur-blob.webp" +import { StripesDecoration } from "@/app/conf/_design-system/stripes-decoration" + +export function BecomeASponsor() { + return ( +
+ +
+
+
+

Become a Sponsor

+

+ Connect with the global GraphQL community and showcase your brand + to industry leaders and decision-makers. +

+
+ +
+
+ + + + + + +
+
+ + ) +} + +function DefinitionListItem({ + className, + term, + definition, +}: { + className?: string + term: string + definition: string +}) { + return ( +
+
+ {term} +
+
{definition}
+
+ ) +} + +function Stripes() { + return ( +
+ +
+ ) +} diff --git a/src/app/conf/2025/components/graphql-foundation-card.tsx b/src/app/conf/2025/components/graphql-foundation-card.tsx index 3002915829..bf1972c678 100644 --- a/src/app/conf/2025/components/graphql-foundation-card.tsx +++ b/src/app/conf/2025/components/graphql-foundation-card.tsx @@ -7,7 +7,7 @@ export function GraphQLFoundationCard({ className }: { className?: string }) {
- +

GraphQLConf is presented by the GraphQL Foundation, uniting the global diff --git a/src/app/conf/2025/components/hero/index.tsx b/src/app/conf/2025/components/hero/index.tsx index 5cac0ba2a3..7868528c6c 100644 --- a/src/app/conf/2025/components/hero/index.tsx +++ b/src/app/conf/2025/components/hero/index.tsx @@ -14,7 +14,7 @@ export function Hero() { return (

- +

@@ -35,7 +35,7 @@ export function Hero() {

- + @@ -55,7 +55,7 @@ export function Hero() { ) } -function DateAndLocation() { +export function HeroDateAndLocation() { return (
@@ -77,7 +77,7 @@ const maskEven = const maskOdd = "repeating-linear-gradient(to right, black, black 12px, transparent 12px, transparent 24px)" -function Stripes() { +export function HeroStripes() { return ( () + export interface ImageLoadedProps extends React.HTMLAttributes { image: string | StaticImageData } export function ImageLoaded({ image, ...rest }: ImageLoadedProps) { const [loaded, setLoaded] = useState(false) + const src = typeof image === "string" ? image : image.src + + const alreadyLoaded = _cache.get(src)?.complete useEffect(() => { - const img = new Image() - const src = typeof image === "string" ? image : image.src - img.src = src - img.onload = () => setLoaded(true) - }, [image]) + let img: HTMLImageElement + if (_cache.has(src)) { + img = _cache.get(src)! + if (img.complete) { + setLoaded(true) + } else { + img.addEventListener("load", () => setLoaded(true)) + } + } else { + img = new Image() + img.src = src + img.addEventListener("load", () => setLoaded(true)) + _cache.set(src, img) + } + }, [src]) - return
+ return
} diff --git a/src/app/conf/2025/components/marquee-rows/index.tsx b/src/app/conf/2025/components/marquee-rows/index.tsx index af8647a28e..5706113f3f 100644 --- a/src/app/conf/2025/components/marquee-rows/index.tsx +++ b/src/app/conf/2025/components/marquee-rows/index.tsx @@ -33,7 +33,7 @@ export function MarqueeRows({ return (
{ + // todo: block scrolling on body setMobileDrawerOpen(prev => !prev) }, []) diff --git a/src/app/conf/2025/components/sponsors.tsx b/src/app/conf/2025/components/sponsors.tsx index 573eb8ae2c..07a250de66 100644 --- a/src/app/conf/2025/components/sponsors.tsx +++ b/src/app/conf/2025/components/sponsors.tsx @@ -1,10 +1,4 @@ -import Stellate from "public/img/conf/Sponsors/Stellate.svg?svgr" -import Hasura from "public/img/conf/Sponsors/Hasura.svg?svgr" -import TheGuild from "public/img/conf/Sponsors/TheGuild.svg?svgr" -import Apollo from "public/img/conf/Sponsors/Apollo.svg?svgr" -import Tyk from "public/img/conf/Sponsors/Tyk.svg?svgr" -import IBM from "public/img/conf/Sponsors/IBM.svg?svgr" -import Graphweaver from "public/img/conf/Sponsors/Graphweaver.svg?svgr" +import Grafbase from "public/img/conf/Sponsors/Grafbase.svg?svgr" import { clsx } from "clsx" import { ChevronRight } from "../pixelarticons/chevron-right" @@ -15,20 +9,12 @@ interface Sponsor { link: string } -const sponsorDiamond: Sponsor[] = [ - { icon: TheGuild, name: "The Guild", link: "https://the-guild.dev" }, - { icon: IBM, name: "IBM", link: "https://www.ibm.com/products/api-connect" }, -] +const sponsorDiamond: Sponsor[] = [] -const sponsorGold: Sponsor[] = [ - { icon: Apollo, name: "Apollo", link: "https://www.apollographql.com/" }, - { icon: Graphweaver, name: "Graphweaver", link: "https://graphweaver.com" }, - { icon: Hasura, name: "Hasura", link: "https://hasura.io" }, -] +const sponsorGold: Sponsor[] = [] const sponsorSilver: Sponsor[] = [ - { icon: Stellate, name: "Stellate", link: "https://stellate.co" }, - { icon: Tyk, name: "Tyk", link: "https://tyk.io/" }, + { icon: Grafbase, name: "Grafbase", link: "https://grafbase.com/" }, ] export interface SponsorsProps { @@ -57,13 +43,13 @@ const sponsorTiers: Tier[] = [ export function Sponsors({ heading }: SponsorsProps) { return ( -
+

{heading}

- {sponsorTiers.map(tier => ( - - ))} + {sponsorTiers.map( + tier => tier.items.length > 0 && , + )}
) @@ -78,7 +64,7 @@ function Tier({ tier }: { tier: Tier }) {
{tier.items.map(({ link, icon: Icon, name }, i) => ( diff --git a/src/app/conf/2025/components/what-to-expect.tsx b/src/app/conf/2025/components/what-to-expect.tsx index 539701cb1e..1666bb1a22 100644 --- a/src/app/conf/2025/components/what-to-expect.tsx +++ b/src/app/conf/2025/components/what-to-expect.tsx @@ -13,12 +13,12 @@ export default function WhatToExpectSection({ {...rest} >

What to expect

-
+
    -
+
) } diff --git a/src/app/conf/2025/layout.tsx b/src/app/conf/2025/layout.tsx index bdd77b4c68..4bb4bfeb77 100644 --- a/src/app/conf/2025/layout.tsx +++ b/src/app/conf/2025/layout.tsx @@ -40,9 +40,9 @@ export default function Layout({
-
- - - - - - - - - +
+
+ + +
+ +
+ + + + + + +
) diff --git a/src/app/conf/2025/pixelarticons/arrow-down.svg b/src/app/conf/2025/pixelarticons/arrow-down.svg index 642c02b9f3..bc9157f91b 100644 --- a/src/app/conf/2025/pixelarticons/arrow-down.svg +++ b/src/app/conf/2025/pixelarticons/arrow-down.svg @@ -1,3 +1,13 @@ - - + + diff --git a/src/app/conf/2025/pixelarticons/caret-down.svg b/src/app/conf/2025/pixelarticons/caret-down.svg new file mode 100644 index 0000000000..84e4fc4392 --- /dev/null +++ b/src/app/conf/2025/pixelarticons/caret-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/app/conf/2025/pixelarticons/close.svg b/src/app/conf/2025/pixelarticons/close.svg index d48de61287..c11e70ca27 100644 --- a/src/app/conf/2025/pixelarticons/close.svg +++ b/src/app/conf/2025/pixelarticons/close.svg @@ -1,3 +1,5 @@ - + diff --git a/src/app/conf/2025/schedule/[id]/page.tsx b/src/app/conf/2025/schedule/[id]/page.tsx new file mode 100644 index 0000000000..88ed38bda3 --- /dev/null +++ b/src/app/conf/2025/schedule/[id]/page.tsx @@ -0,0 +1,200 @@ +import { notFound } from "next/navigation" +import { Metadata } from "next" +import clsx from "clsx" +import { format, parseISO } from "date-fns" + +import { metadata as layoutMetadata } from "@/app/conf/2023/layout" +import { Avatar } from "../../../_components/speakers/avatar" +import { + SocialMediaIcon, + SocialMediaIconServiceType, +} from "../../../_components/speakers/social-media" +import { speakers, schedule } from "../../_data" +import { ScheduleSession } from "../../../2023/types" + +import { SessionVideo } from "./session-video" +import { NavbarPlaceholder } from "../../components/navbar" +import { BackLink } from "../_components/back-link" +import { Tag } from "@/app/conf/_design-system/tag" +import { eventsColors } from "../../utils" + +function getEventTitle(event: ScheduleSession, speakers: string[]): string { + let { name } = event + + if (!speakers) { + return name + } + + speakers?.forEach(speaker => { + const speakerInTitle = name.indexOf(`- ${speaker.replace("ı", "i")}`) + if (speakerInTitle > -1) { + name = name.slice(0, speakerInTitle) + } + }) + + return name +} + +type SessionProps = { params: { id: string } } + +export function generateMetadata({ params }: SessionProps): Metadata { + const event = schedule.find(s => s.id === params.id)! + + const keywords = [ + event.event_type, + event.audience, + event.event_subtype, + ...(event.speakers || []).map(s => s.name), + ].filter(Boolean) + + return { + title: event.name, + description: event.description, + keywords: [...layoutMetadata.keywords, ...keywords], + openGraph: { + images: `/img/__og-image/2024/${event.id}.png`, + }, + } +} + +export function generateStaticParams() { + return schedule.filter(s => s.id).map(s => ({ id: s.id })) +} + +export default function SessionPage({ params }: SessionProps) { + const event = schedule.find(s => s.id === params.id) + if (!event) { + notFound() + } + + // @ts-expect-error -- fixme + event.speakers = (event.speakers || []).map(speaker => + speakers.find(s => s.username === speaker.username), + ) + + const eventType = event.event_type.endsWith("s") + ? event.event_type.slice(0, -1) + : event.event_type + + const eventTitle = getEventTitle( + event, + event.speakers!.map(s => s.name), + ) + + return ( +
+ +
+
+
+ + + +
+
+
+ {eventType && ( + + {eventType} + + )} + {event.audience && ( + + {event.audience} + + )} + {event.event_subtype && ( + + {event.event_subtype} + + )} +
+

{eventTitle}

+ +
+
+ {event.speakers!.map(speaker => ( +
1 ? "max-w-[320px]" : ""}`} + key={speaker.username} + > + + +
+ + {speaker.name} + + + + {speaker.company} + {speaker.company && ", "} + {speaker.position} + + {speaker.socialurls?.length ? ( +
+
+ {speaker.socialurls.map(social => ( + + + + ))} +
+
+ ) : null} +
+
+ ))} +
+

{event.description}

+ +
+ {event.files?.map(({ path }) => ( +
+ + View Full PDF{" "} + + +