diff --git a/package.json b/package.json index b7d60df083..e14f3423c9 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "dependencies": { "@graphql-tools/schema": "10.0.15", - "@headlessui/react": "^1.7.17", + "@headlessui/react": "^2.2.4", "@radix-ui/react-radio-group": "^1.1.3", "@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/nesting": "0.0.0-insiders.565cd3e", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c2cde5d2db..c351fc07b7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,8 +17,8 @@ importers: specifier: 10.0.15 version: 10.0.15(graphql@16.10.0) '@headlessui/react': - specifier: ^1.7.17 - version: 1.7.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^2.2.4 + version: 2.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-radio-group': specifier: ^1.1.3 version: 1.2.2(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -988,6 +988,27 @@ packages: resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@floating-ui/core@1.7.0': + resolution: {integrity: sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==} + + '@floating-ui/dom@1.7.0': + resolution: {integrity: sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==} + + '@floating-ui/react-dom@2.1.2': + resolution: {integrity: sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/react@0.26.28': + resolution: {integrity: sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.9': + resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} + '@graphql-eslint/eslint-plugin@4.3.0': resolution: {integrity: sha512-9UTJfYNGAK5GuFapsNvA+508ke8YPc9Yt6mgT4Lc+gS18p53oG5wmXd3jdmNeVOfxhUefJcJbn925vIrjg/8/g==} engines: {node: '>=18'} @@ -1132,6 +1153,13 @@ packages: react: ^16 || ^17 || ^18 react-dom: ^16 || ^17 || ^18 + '@headlessui/react@2.2.4': + resolution: {integrity: sha512-lz+OGcAH1dK93rgSMzXmm1qKOJkBUqZf1L4M8TWLNplftQD3IkoEDdUFNfAn4ylsN6WOTVtWaLmvmaHOUk1dTA==} + engines: {node: '>=10'} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + react-dom: ^18 || ^19 || ^19.0.0-rc + '@humanwhocodes/config-array@0.13.0': resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} engines: {node: '>=10.10.0'} @@ -1619,6 +1647,43 @@ packages: '@types/react': optional: true + '@react-aria/focus@3.20.3': + resolution: {integrity: sha512-rR5uZUMSY4xLHmpK/I8bP1V6vUNHFo33gTvrvNUsAKKqvMfa7R2nu5A6v97dr5g6tVH6xzpdkPsOJCWh90H2cw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-aria/interactions@3.25.1': + resolution: {integrity: sha512-ntLrlgqkmZupbbjekz3fE/n3eQH2vhncx8gUp0+N+GttKWevx7jos11JUBjnJwb1RSOPgRUFcrluOqBp0VgcfQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-aria/ssr@3.9.8': + resolution: {integrity: sha512-lQDE/c9uTfBSDOjaZUJS8xP2jCKVk4zjQeIlCH90xaLhHDgbpCdns3xvFpJJujfj3nI4Ll9K7A+ONUBDCASOuw==} + engines: {node: '>= 12'} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-aria/utils@3.29.0': + resolution: {integrity: sha512-jSOrZimCuT1iKNVlhjIxDkAhgF7HSp3pqyT6qjg/ZoA0wfqCi/okmrMPiWSAKBnkgX93N8GYTLT3CIEO6WZe9Q==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-stately/flags@3.1.1': + resolution: {integrity: sha512-XPR5gi5LfrPdhxZzdIlJDz/B5cBf63l4q6/AzNqVWFKgd0QqY5LvWJftXkklaIUpKSJkIKQb8dphuZXDtkWNqg==} + + '@react-stately/utils@3.10.6': + resolution: {integrity: sha512-O76ip4InfTTzAJrg8OaZxKU4vvjMDOpfA/PGNOytiXwBbkct2ZeZwaimJ8Bt9W1bj5VsZ81/o/tW4BacbdDOMA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-types/shared@3.29.1': + resolution: {integrity: sha512-KtM+cDf2CXoUX439rfEhbnEdAgFZX20UP2A35ypNIawR7/PFFPjQDWyA2EnClCcW/dLWJDEPX2U8+EJff8xqmQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + '@repeaterjs/repeater@3.0.6': resolution: {integrity: sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA==} @@ -1709,6 +1774,9 @@ packages: '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + '@swc/helpers@0.5.17': + resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} + '@swc/helpers@0.5.5': resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} @@ -1727,12 +1795,21 @@ packages: peerDependencies: tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20' + '@tanstack/react-virtual@3.13.9': + resolution: {integrity: sha512-SPWC8kwG/dWBf7Py7cfheAPOxuvIv4fFQ54PdmYbg7CpXfsKxkucak43Q0qKsxVthhUJQ1A7CIMAIplq4BjVwA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@tanstack/react-virtual@3.8.4': resolution: {integrity: sha512-Dq0VQr3QlTS2qL35g360QaJWBt7tCn/0xw4uZ0dHXPLO1Ak4Z4nVX4vuj1Npg1b/jqNMDToRtR5OIxM2NXRBWg==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + '@tanstack/virtual-core@3.13.9': + resolution: {integrity: sha512-3jztt0jpaoJO5TARe2WIHC1UQC3VMLAFUW5mmMo0yrkwtDB2AQP0+sh10BVUpWrnvHjSLvzFizydtEGLCJKFoQ==} + '@tanstack/virtual-core@3.8.4': resolution: {integrity: sha512-iO5Ujgw3O1yIxWDe9FgUPNkGjyT657b1WNX52u+Wv1DyBFEpdCdGkuVaky0M3hHFqNWjAmHWTn4wgj9rTr7ZQg==} @@ -4508,6 +4585,9 @@ packages: resolution: {integrity: sha512-7RnqIMq572L8PeEzKeBINYEJDDxpcH8JEgLwUqBd3TkofhFRbkq4QLR0u+36avGAhCRbk2nnmjcW9SE531hPDg==} engines: {node: ^14.18.0 || >=16.0.0} + tabbable@6.2.0: + resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} + tailwindcss@3.4.17: resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} engines: {node: '>=14.0.0'} @@ -4733,6 +4813,11 @@ packages: react-router-dom: optional: true + use-sync-external-store@1.5.0: + resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -5770,6 +5855,31 @@ snapshots: '@eslint/js@8.57.1': {} + '@floating-ui/core@1.7.0': + dependencies: + '@floating-ui/utils': 0.2.9 + + '@floating-ui/dom@1.7.0': + dependencies: + '@floating-ui/core': 1.7.0 + '@floating-ui/utils': 0.2.9 + + '@floating-ui/react-dom@2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/dom': 1.7.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@floating-ui/react@0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@floating-ui/utils': 0.2.9 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tabbable: 6.2.0 + + '@floating-ui/utils@0.2.9': {} + '@graphql-eslint/eslint-plugin@4.3.0(@types/node@22.10.5)(eslint@8.57.1)(graphql@16.10.0)(typescript@5.7.2)': dependencies: '@graphql-tools/code-file-loader': 8.1.10(graphql@16.10.0) @@ -5996,6 +6106,16 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@headlessui/react@2.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react': 0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/focus': 3.20.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/interactions': 3.25.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/react-virtual': 3.13.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + use-sync-external-store: 1.5.0(react@18.3.1) + '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 @@ -6401,6 +6521,55 @@ snapshots: optionalDependencies: '@types/react': 18.3.18 + '@react-aria/focus@3.20.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-aria/interactions': 3.25.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/utils': 3.29.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-types/shared': 3.29.1(react@18.3.1) + '@swc/helpers': 0.5.17 + clsx: 2.1.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@react-aria/interactions@3.25.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-aria/ssr': 3.9.8(react@18.3.1) + '@react-aria/utils': 3.29.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-stately/flags': 3.1.1 + '@react-types/shared': 3.29.1(react@18.3.1) + '@swc/helpers': 0.5.17 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@react-aria/ssr@3.9.8(react@18.3.1)': + dependencies: + '@swc/helpers': 0.5.17 + react: 18.3.1 + + '@react-aria/utils@3.29.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-aria/ssr': 3.9.8(react@18.3.1) + '@react-stately/flags': 3.1.1 + '@react-stately/utils': 3.10.6(react@18.3.1) + '@react-types/shared': 3.29.1(react@18.3.1) + '@swc/helpers': 0.5.17 + clsx: 2.1.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@react-stately/flags@3.1.1': + dependencies: + '@swc/helpers': 0.5.17 + + '@react-stately/utils@3.10.6(react@18.3.1)': + dependencies: + '@swc/helpers': 0.5.17 + react: 18.3.1 + + '@react-types/shared@3.29.1(react@18.3.1)': + dependencies: + react: 18.3.1 + '@repeaterjs/repeater@3.0.6': {} '@shikijs/core@1.2.1': {} @@ -6508,6 +6677,10 @@ snapshots: '@swc/counter@0.1.3': {} + '@swc/helpers@0.5.17': + dependencies: + tslib: 2.8.1 + '@swc/helpers@0.5.5': dependencies: '@swc/counter': 0.1.3 @@ -6530,12 +6703,20 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 3.4.17 + '@tanstack/react-virtual@3.13.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/virtual-core': 3.13.9 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@tanstack/react-virtual@3.8.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@tanstack/virtual-core': 3.8.4 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@tanstack/virtual-core@3.13.9': {} + '@tanstack/virtual-core@3.8.4': {} '@theguild/remark-mermaid@0.0.7(react@18.3.1)': @@ -9906,6 +10087,8 @@ snapshots: '@pkgr/core': 0.1.1 tslib: 2.8.1 + tabbable@6.2.0: {} + tailwindcss@3.4.17: dependencies: '@alloc/quick-lru': 5.2.0 @@ -10197,6 +10380,10 @@ snapshots: react-dom: 18.3.1(react@18.3.1) serialize-query-params: 2.0.2 + use-sync-external-store@1.5.0(react@18.3.1): + dependencies: + react: 18.3.1 + util-deprecate@1.0.2: {} uuid@9.0.1: {} diff --git a/src/app/conf/2023/_data.ts b/src/app/conf/2023/_data.ts index 55e10bdab8..a824b472cb 100644 --- a/src/app/conf/2023/_data.ts +++ b/src/app/conf/2023/_data.ts @@ -77,8 +77,6 @@ async function getSchedule(): Promise { 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/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/2024/_data.ts b/src/app/conf/2024/_data.ts index e172f3e519..5c5d97614c 100644 --- a/src/app/conf/2024/_data.ts +++ b/src/app/conf/2024/_data.ts @@ -77,8 +77,6 @@ async function getSchedule(): Promise { 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/_data.ts b/src/app/conf/2025/_data.ts new file mode 100644 index 0000000000..9b75bdfcba --- /dev/null +++ b/src/app/conf/2025/_data.ts @@ -0,0 +1,128 @@ +import "server-only" +import { stripHtml } from "string-strip-html" +import { SchedSpeaker, ScheduleSession } from "@/app/conf/2023/types" +import pLimit from "p-limit" + +const USE_2025 = false + +const apiUrl = USE_2025 + ? "https://graphqlconf2025.sched.com/api" + : "https://graphqlconf2024.sched.com/api" + +const token = USE_2025 + ? process.env.SCHED_ACCESS_TOKEN_2025 + : process.env.SCHED_ACCESS_TOKEN_2024 + +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()}`, + ) + } +} + +async function getUsernames(): Promise { + const response = await fetchData<{ username: string }[]>( + `${apiUrl}/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( + `${apiUrl}/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, + } + }) + .sort((a, b) => { + if (a.avatar && !b.avatar) return -1 + if (!a.avatar && b.avatar) return 1 + return 0 + }) + + return result +} + +async function getSchedule(): Promise { + const sessions = await fetchData( + `${apiUrl}/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}"`) + } + + // TODO: Preserve formatting?? + return { + ...session, + description: description && stripHtml(description).result, + } + }) + + return result +} + +export const speakers = await getSpeakers() + +// TODO: Collect tags from schedule for speakers. +export const schedule = await getSchedule() + +type SpeakerUsername = SchedSpeaker["username"] + +export const speakerSessions = new Map() + +for (const session of schedule) { + for (const speaker of session.speakers || []) { + if (!speakerSessions.has(speaker.username)) { + speakerSessions.set(speaker.username, []) + } + + speakerSessions.get(speaker.username)!.push(session) + } +} + +export const returningSpeakers = new Set() + +import { speakers as speakers2024 } from "../2024/_data" +import { speakers as speakers2023 } from "../2023/_data" + +for (const { username } of speakers2024) { + if (speakerSessions.has(username)) { + returningSpeakers.add(username) + } +} + +for (const { username } of speakers2023) { + if (speakerSessions.has(username)) { + returningSpeakers.add(username) + } +} 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/components/call-for-proposals.tsx b/src/app/conf/2025/components/call-for-proposals.tsx index a0fe219db6..92310dbbcd 100644 --- a/src/app/conf/2025/components/call-for-proposals.tsx +++ b/src/app/conf/2025/components/call-for-proposals.tsx @@ -45,7 +45,7 @@ function DefinitionListItem({ definition: string }) { return ( -
+
{term}
@@ -140,7 +140,7 @@ function NotesTab() { event. -

Preparing to Submit Your Proposal

+

Preparing to Submit Your Proposal

While it is not our intention to provide you with strict instructions on how to prepare your proposal, we hope you will take a moment to review @@ -161,7 +161,7 @@ function NotesTab() { letting you share your experiences, educate the community about an issue, or generate interest in a project.

-

How to Give a Great Talk

+

How to Give a Great Talk

We want to make sure submitters receive resources to help put together a great submission and if accepted, give the best presentation possible. @@ -176,7 +176,7 @@ function NotesTab() { .

-

+

Have More Questions? First Time Submitting? Don't Feel Intimidated

@@ -238,7 +238,7 @@ function ProcessTab() {

  • The new Subject Matter Experts initiative (SMEs)
  • The Program Committee
  • -

    The Technical Steering Committee

    +

    The Technical Steering Committee

    The TSC are a group of 11 individuals who are elected to serve a two year term to provide technical oversight of all GraphQL development @@ -252,7 +252,7 @@ function ProcessTab() {

  • Quality of Presentation
  • Importance
  • -

    Subject Matter Experts

    +

    Subject Matter Experts

    The SME initiative is new for 2025. This will be a panel of volunteers drawn from industry experts, working group members, security and @@ -266,7 +266,7 @@ function ProcessTab() {

  • Originality
  • Audience Engagement
  • -

    The Program Committee

    +

    The Program Committee

    The Program Committee is made up of representatives from the GraphQL Foundation board and interested members of the GraphQL community who @@ -276,7 +276,7 @@ function ProcessTab() { demographics, to ensure a varied and well-rounded representation of the GraphQL ecosystem.

    -

    +

    Have More Questions? First Time Submitting? Don't Feel Intimidated

    @@ -349,7 +349,7 @@ export function CallForProposals() { .

    -

    +

    Please be aware that the Linux Foundation uses Sessionize for CFP submissions. Sessionize is a cloud-based event content management software designed to be intuitive and user-friendly. If you need @@ -443,7 +443,7 @@ function TabButton({ tabIndex={tabIndex} aria-selected={activeTab === tab} className={clsx( - "gql-focus-visible flex items-center justify-between px-3 py-4 typography-body-lg hover:bg-sec-light focus:outline-none max-lg:border-b max-lg:border-sec-dark max-lg:first:border-t lg:[--collapsible:1] lg:aria-selected:bg-sec-light", + "gql-focus-visible typography-body-lg flex items-center justify-between px-3 py-4 hover:bg-sec-light focus:outline-none max-lg:border-b max-lg:border-sec-dark max-lg:first:border-t lg:[--collapsible:1] lg:aria-selected:bg-sec-light", className, )} onFocus={() => { diff --git a/src/app/conf/2025/components/cta-card-section/index.tsx b/src/app/conf/2025/components/cta-card-section/index.tsx index 612ce2508a..30f8c542d3 100644 --- a/src/app/conf/2025/components/cta-card-section/index.tsx +++ b/src/app/conf/2025/components/cta-card-section/index.tsx @@ -2,8 +2,9 @@ import { StripesDecoration } from "@/app/conf/_design-system/stripes-decoration" import logoMask from "./logo-mask.webp" -export interface CtaCardSectionProps extends React.HTMLAttributes { - title: string +export interface CtaCardSectionProps + extends Omit, "title"> { + title: React.ReactNode description: string children: React.ReactNode } @@ -23,10 +24,10 @@ export function CtaCardSection({ >

    -

    +

    {heading}

    -

    +

    {description}

    diff --git a/src/app/conf/2025/components/hero/hero-image.tsx b/src/app/conf/2025/components/hero/hero-image.tsx new file mode 100644 index 0000000000..d1c58d7415 --- /dev/null +++ b/src/app/conf/2025/components/hero/hero-image.tsx @@ -0,0 +1,17 @@ +import Image from "next-image-export-optimizer" + +import heroPhoto from "./hero-photo.jpeg" + +export function HeroImage() { + return ( +
    + five speakers at GraphQLConf 2024 +
    + ) +} diff --git a/src/app/conf/2025/components/hero/index.tsx b/src/app/conf/2025/components/hero/index.tsx index aab17fa7e9..747fdd7363 100644 --- a/src/app/conf/2025/components/hero/index.tsx +++ b/src/app/conf/2025/components/hero/index.tsx @@ -1,63 +1,60 @@ -import Image from "next-image-export-optimizer" +import { CalendarIcon } from "@/app/conf/_design-system/pixelarticons/calendar-icon" +import { PinIcon } from "@/app/conf/_design-system/pixelarticons/pin-icon" -import { Button } from "../../../_design-system/button" -import { CalendarIcon } from "../../../_design-system/pixelarticons/calendar-icon" -import { GET_TICKETS_LINK } from "../../links" -import { PinIcon } from "../../../_design-system/pixelarticons/pin-icon" -import graphqlFoundationWordmarkSvg from "../../assets/graphql-foundation-wordmark.svg" +import GraphQLFoundationWordmark from "../../assets/graphql-foundation-wordmark.svg?svgr" import { ImageLoaded } from "../image-loaded" import blurBean from "./blur-bean-cropped.webp" -import heroPhoto from "./hero-photo.jpeg" -export function Hero() { +export function Hero({ + pageName, + year, + children, + bottom, +}: { + pageName?: string + year: string + children: React.ReactNode + bottom?: React.ReactNode +}) { return (
    - +
    -

    - GraphQLConf - 2025 -

    + {pageName ? ( +
    + + GraphQLConf {year} + +

    {pageName}

    +
    + ) : ( +

    + GraphQLConf + {year} +

    + )}
    - + hosted by - GraphQL Foundation +
    -
    - - -
    +
    {children}
    -
    - five speakers at GraphQLConf 2024 -
    + {bottom}
    ) } -function DateAndLocation() { +export function HeroDateAndLocation() { return ( -
    +
    @@ -77,7 +74,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/navbar.tsx b/src/app/conf/2025/components/navbar.tsx index a2b36ae5d0..3aefc04064 100644 --- a/src/app/conf/2025/components/navbar.tsx +++ b/src/app/conf/2025/components/navbar.tsx @@ -21,6 +21,7 @@ export function Navbar({ links, year }: NavbarProps): ReactElement { const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false) const handleDrawerClick = useCallback(() => { + // todo: block scrolling on body setMobileDrawerOpen(prev => !prev) }, []) @@ -60,7 +61,7 @@ export function Navbar({ links, year }: NavbarProps): ReactElement {
    -
    +

    - @@ -81,7 +82,7 @@ export function Navbar({ links, year }: NavbarProps): ReactElement {

    ) } -function LinkedInIcon(props: HTMLAttributes) { - return ( - - - - ) -} - function Stripes({ mask }: { mask?: string }) { return (
    -

    How to get to the venue?

    +

    How to get to the venue?

    -

    Where to stay?

    +

    Where to stay?

    {number} {" "} - {text} + {text} ) } diff --git a/src/app/conf/2025/faq.tsx b/src/app/conf/2025/faq.tsx index 917c3000dc..8e34eb949b 100644 --- a/src/app/conf/2025/faq.tsx +++ b/src/app/conf/2025/faq.tsx @@ -209,7 +209,7 @@ export function FAQ({ className }: { className?: string }) {

    Frequently Asked Questions

    -

    +

    You can find much more information on our{" "} Resources @@ -226,12 +226,12 @@ export function FAQ({ className }: { className?: string }) { className="group/q w-full border border-neu-400 @container" >

    - + {faq.question} -
    {faq.answer}
    +
    {faq.answer}
    ))}
    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({ - + }> + + +
    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 eventTitle = getEventTitle( + event, + event.speakers!.map(s => s.name), + ) + + const video = findVideo(event, eventTitle) + + return ( + <> + +
    +
    +
    +
    +
    + + {video ? ( + + ) : ( +
    + )} + +
    +

    + Session description +

    +

    {event.description}

    +
    + +
    + +

    + Session speakers +

    + + +
    + +

    + Session resources +

    +
    + {event.files?.map(({ path }) => ( +