Skip to content

Commit aaf5a12

Browse files
committed
Build out an interactive editor for the landing
1 parent 812b8b4 commit aaf5a12

File tree

9 files changed

+248
-64
lines changed

9 files changed

+248
-64
lines changed

pnpm-lock.yaml

Lines changed: 27 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/conf/2025/components/navbar.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ export function Navbar({ links, year }: NavbarProps): ReactElement {
2121
const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false)
2222

2323
const handleDrawerClick = useCallback(() => {
24-
// todo: block scrolling on body
2524
setMobileDrawerOpen(prev => !prev)
2625
}, [])
2726

src/app/conf/2025/components/register-today/index.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ export function RegisterToday({ className }: RegisterTodayProps) {
1818
className,
1919
)}
2020
>
21-
{/* todo: placeholders work in preview, but they could use some improvement */}
2221
<NextImage
2322
src={speakerImage}
2423
alt="GraphQL Conference"

src/components/index-page/how-it-works.tsx

Lines changed: 0 additions & 50 deletions
This file was deleted.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export function HowItWorksListItem({
2+
text,
3+
code,
4+
icon,
5+
}: {
6+
text: React.ReactNode
7+
code: React.ReactNode
8+
icon?: React.ReactNode
9+
}) {
10+
return (
11+
<li className="flex flex-col [counter-increment:list-item]">
12+
<div className="typography-body-md flex items-center bg-neu-0 py-4 pr-2 before:typography-body-sm before:mr-2 before:inline-flex before:size-5 before:translate-y-[-0.5px] before:items-center before:justify-center before:bg-neu-200 before:p-1 before:text-neu-800 before:content-[counter(list-item)] dark:before:bg-neu-50 md:p-6">
13+
{text}
14+
{icon}
15+
</div>
16+
<div className="mt-px flex-1 bg-neu-0 text-sm md:pl-2 md:pt-2 max-md:[&_code>span]:!pl-0 [&_pre]:border-none [&_pre]:ring-0">
17+
{code}
18+
</div>
19+
</li>
20+
)
21+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Button } from "@/app/conf/_design-system/button"
2+
import { SectionLabel } from "@/app/conf/_design-system/section-label"
3+
import { CodeA, CodeB, CodeC } from "../../code-blocks"
4+
5+
import { useRef } from "react"
6+
import { useInView } from "motion/react"
7+
import { HowItWorksListItem } from "./how-it-works-list-item"
8+
import dynamic from "next/dynamic"
9+
import { PlayButton } from "./play-button"
10+
11+
const InteractiveEditor = dynamic(import("./interactive-editor"), {
12+
ssr: false,
13+
})
14+
15+
const TRY_IT_OUT_URL = "https://graphql.org/swapi-graphql"
16+
17+
export function HowItWorks() {
18+
const sectionRef = useRef<HTMLElement>(null)
19+
const inView = useInView(sectionRef)
20+
21+
return (
22+
<section ref={sectionRef} className="gql-container gql-section xl:py-20">
23+
<SectionLabel className="mb-6">How it works</SectionLabel>
24+
<h2 className="typography-h2 mb-6 lg:mb-16">A GraphQL Query</h2>
25+
<ol className="gql-radial-gradient list-none gap-px max-md:bg-gradient-to-r max-md:from-transparent max-md:via-neu-400 max-md:to-transparent lg:grid lg:grid-cols-3">
26+
<HowItWorksListItem text="Describe your data" code={<CodeA />} />
27+
{/* TODO: There's a blink on transition. We need to mount the new editor before unmounting the old one. */}
28+
{inView ? (
29+
<InteractiveEditor />
30+
) : (
31+
<>
32+
<HowItWorksListItem
33+
text="Ask for what you want"
34+
icon={<PlayButton />}
35+
code={<CodeB />}
36+
/>
37+
<HowItWorksListItem
38+
text="Get predictable results"
39+
code={<CodeC />}
40+
/>
41+
</>
42+
)}
43+
</ol>
44+
45+
<Button className="mx-auto mt-8 w-fit lg:mt-16" href={TRY_IT_OUT_URL}>
46+
Try GraphiQL
47+
</Button>
48+
</section>
49+
)
50+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import React, { useState } from "react"
2+
import { graphql, GraphQLSchema } from "graphql"
3+
4+
import { QueryEditor } from "@/components/interactive-code-block/query-editor"
5+
import { ResultViewer } from "@/components/interactive-code-block/result-viewer"
6+
7+
import { HowItWorksListItem } from "./how-it-works-list-item"
8+
import { getVariableToType } from "../../interactive-code-block/get-variable-to-type"
9+
import { CodeBlockLabel } from "../../pre/code-block-label"
10+
import { VariableEditor } from "../../interactive-code-block/variable-editor"
11+
import { PlayButton } from "./play-button"
12+
import { run } from "node:test"
13+
14+
const INITIAL_QUERY_TEXT = `{
15+
project(name: "GraphQL") {
16+
tagline
17+
}
18+
}`
19+
20+
const INITIAL_RESULTS_TEXT = `{
21+
"project": {
22+
"tagline": "A query language for APIs"
23+
}
24+
}`
25+
26+
const schema = new GraphQLSchema({
27+
types: [],
28+
})
29+
30+
export default function InteractiveEditor() {
31+
const [query, setQuery] = useState(INITIAL_QUERY_TEXT)
32+
const [results, setResults] = useState(INITIAL_RESULTS_TEXT)
33+
const [variableTypes, setVariableTypes] = useState<Record<string, string>>({})
34+
const [variables, setVariables] = useState("")
35+
const editorQueryId = React.useRef(0)
36+
37+
async function runQuery(options: { manual: boolean }) {
38+
editorQueryId.current++
39+
const queryID = editorQueryId.current
40+
try {
41+
const result = await graphql({
42+
schema: schema,
43+
source: query,
44+
variableValues: {},
45+
})
46+
47+
let resultToSerialize: any = result
48+
if (result.errors) {
49+
if (!options.manual) {
50+
// if the query was ran on edit, we display errors on the left side
51+
// so we can just return instead of showing the resulting error
52+
return
53+
}
54+
55+
// Convert errors to serializable format
56+
const serializedErrors = result.errors.map(error => ({
57+
message: error.message,
58+
locations: error.locations,
59+
path: error.path,
60+
}))
61+
// Replace errors with serialized version for JSON.stringify
62+
resultToSerialize = { ...result, errors: serializedErrors }
63+
}
64+
65+
if (queryID === editorQueryId.current) {
66+
setResults(JSON.stringify(resultToSerialize, null, 2))
67+
}
68+
} catch (error) {
69+
if (queryID === editorQueryId.current) {
70+
setResults(JSON.stringify(error, null, 2))
71+
}
72+
}
73+
}
74+
75+
const editor = (
76+
<QueryEditor
77+
value={query}
78+
schema={schema}
79+
onEdit={() => {
80+
setQuery(query)
81+
runQuery({ manual: false })
82+
}}
83+
runQuery={() => {
84+
setVariableTypes(getVariableToType(schema, query))
85+
runQuery({ manual: true })
86+
}}
87+
/>
88+
)
89+
90+
return (
91+
<>
92+
<HowItWorksListItem
93+
text="Ask for what you want"
94+
icon={
95+
<PlayButton
96+
onClick={() => {
97+
void runQuery({ manual: true })
98+
}}
99+
/>
100+
}
101+
code={
102+
Object.keys(variableTypes).length > 0 ? (
103+
<div className="hasVariables flex flex-col">
104+
{editor}
105+
<div className="flex flex-col border-neu-200 dark:border-neu-50">
106+
<CodeBlockLabel
107+
text="Variables"
108+
className="border-b border-neu-200 bg-[--cm-background] dark:border-neu-50"
109+
/>
110+
<VariableEditor
111+
value={variables}
112+
variableToType={variableTypes}
113+
onEdit={setVariables}
114+
onRunQuery={() => void runQuery({ manual: false })}
115+
/>
116+
</div>
117+
</div>
118+
) : (
119+
editor
120+
)
121+
}
122+
/>
123+
<HowItWorksListItem
124+
text="Get predictable results"
125+
code={<ResultViewer value={results} />}
126+
/>
127+
</>
128+
)
129+
}

0 commit comments

Comments
 (0)