Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions src/components/playground/GraphiQLPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React, {useMemo} from "react"
import {GraphiQL} from "graphiql"
import type {Fetcher} from "@graphiql/toolkit"
import {createGraphiQLFetcher} from "@graphiql/create-fetcher"
import {CookiePreferenceCategory, playgroundAdsConversionId} from "@site/src/constants"
import {analyticsHandler, sendConversionEvent} from "@site/src/utils"
import {useCookieConsent} from "@site/src/utils/hooks/useCookieConsent"
import "graphiql/graphiql.css"
import "../../css/graphiql.css"

type GraphiQLPanelProps = {
apiEndpoint: string
}

const emptyGraphiqlStorageObject = {
getItem: (): null => null,
setItem: (): void => undefined,
removeItem: (): void => undefined,
clear: (): void => undefined,
length: 0,
}

const GraphiQLPanel = ({apiEndpoint}: GraphiQLPanelProps) => {
const {getCookieConsent} = useCookieConsent()
const cookieConsent = getCookieConsent()

const graphQLFetcher: Fetcher = (graphQLParams, opts) => {
analyticsHandler("GraphQL", "tc_fetch_query", apiEndpoint)
sendConversionEvent(playgroundAdsConversionId)

const fetcher = createGraphiQLFetcher({url: apiEndpoint}) as unknown as Fetcher
return fetcher(graphQLParams, opts)
}

const graphiqlStorage = useMemo(() => {
if (
cookieConsent?.accepted &&
(!cookieConsent?.preferences || cookieConsent?.preferences?.includes(CookiePreferenceCategory.PREFERENCE))
) {
return undefined
}

return emptyGraphiqlStorageObject
}, [cookieConsent])

return (
<GraphiQL fetcher={graphQLFetcher} storage={graphiqlStorage}>
<GraphiQL.Logo>
<></>
</GraphiQL.Logo>
</GraphiQL>
)
}

export default GraphiQLPanel
58 changes: 10 additions & 48 deletions src/components/playground/Playground.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import React, {useEffect, useMemo, useState} from "react"
import {GraphiQL} from "graphiql"
import {analyticsHandler, isValidURL, sendConversionEvent} from "@site/src/utils"
import {CookiePreferenceCategory, playgroundAdsConversionId} from "@site/src/constants"
import "graphiql/graphiql.css"
import "../../css/graphiql.css"
import {type FetcherParams, FetcherOpts} from "@graphiql/toolkit"
import {useCookieConsent} from "@site/src/utils/hooks/useCookieConsent"
import {createGraphiQLFetcher} from "@graphiql/create-fetcher"
import React, {Suspense, lazy, useEffect, useState} from "react"
import {isValidURL} from "@site/src/utils"

const GraphiQLPanel = lazy(() => import("./GraphiQLPanel"))

const useDebouncedValue = (inputValue: string, delay: number) => {
const [debouncedValue, setDebouncedValue] = useState(inputValue)
Expand All @@ -33,9 +28,6 @@ const Playground = () => {
)
const [inputValue, setInputValue] = useState<string>(initialApiEndpoint.toString())

const {getCookieConsent} = useCookieConsent()
const cookieConsent = getCookieConsent()

const debouncedApiEndpoint = useDebouncedValue(inputValue, 500)
const apiEndpointInputClasses = `border border-solid border-tailCall-border-light-500 rounded-lg font-space-grotesk h-11 w-[100%]
p-SPACE_04 text-content-small outline-none focus:border-x-tailCall-light-700`
Expand All @@ -46,37 +38,7 @@ const Playground = () => {
}
}, [debouncedApiEndpoint])

const graphQLFetcher = async (graphQLParams: FetcherParams, opts?: FetcherOpts) => {
if (apiEndpoint.toString().trim() === "") {
return Promise.resolve({})
}
analyticsHandler("GraphQL", "tc_fetch_query", apiEndpoint.toString())
sendConversionEvent(playgroundAdsConversionId)

const fetcher = createGraphiQLFetcher({url: apiEndpoint.toString()})
return fetcher(graphQLParams, opts)
}

const emptyGraphiqlStorageObject = {
getItem: (): null => null,
setItem: (): void => undefined,
removeItem: (): void => undefined,
clear: (): void => undefined,
length: 0,
}

const graphiqlStorage = useMemo(() => {
if (
cookieConsent?.accepted &&
(!cookieConsent?.preferences || cookieConsent?.preferences?.includes(CookiePreferenceCategory.PREFERENCE))
) {
// Defaults to local storage
return undefined
}

// Block storing graphiql data in local storage if user denies cookie consent
return emptyGraphiqlStorageObject
}, [cookieConsent])
const endpointUrl = apiEndpoint.toString().trim()

return (
<div className="min-h-[90vh]">
Expand All @@ -93,11 +55,11 @@ const Playground = () => {
/>
</div>
<div className="flex my-SPACE_03">
<GraphiQL fetcher={graphQLFetcher} storage={graphiqlStorage}>
<GraphiQL.Logo>
<></>
</GraphiQL.Logo>
</GraphiQL>
{endpointUrl !== "" && (
<Suspense fallback={null}>
<GraphiQLPanel apiEndpoint={endpointUrl} />
</Suspense>
)}
</div>
</div>
)}
Expand Down
1 change: 0 additions & 1 deletion src/components/playground/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from "react"
import Playground from "./Playground"
import Announcement from "@site/src/components/shared/Announcement"

const PlaygroundPage = (): JSX.Element => {
return (
Expand Down
10 changes: 5 additions & 5 deletions src/pages/playground.tsx → src/pages/playground/app.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import React, {useEffect} from "react"
import ReactGA from "react-ga4"
import Layout from "@theme/Layout"
import PlaygroundPage from "../components/playground"
import PlaygroundPage from "../../components/playground"
import {useLocation} from "@docusaurus/router"
import {PageDescription, PageTitle} from "../constants/titles"
import {PageDescription, PageTitle} from "../../constants/titles"

const Playground = () => {
const PlaygroundApp = () => {
const location = useLocation()

useEffect(() => {
ReactGA.send({hitType: "pageview", page: location.pathname, title: "Playground Page"})
ReactGA.send({hitType: "pageview", page: location.pathname, title: "Playground App Page"})
}, [])

return (
Expand All @@ -19,4 +19,4 @@ const Playground = () => {
)
}

export default Playground
export default PlaygroundApp
204 changes: 204 additions & 0 deletions static/playground/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="description"
content="Open Tailcall's GraphQL playground with a server endpoint."
/>
<title>Tailcall Playground</title>
<link rel="icon" href="/images/favicon.ico" />
<style>
:root {
color-scheme: dark;
--bg: #080a0f;
--panel: #11151d;
--line: #283241;
--text: #f5f7fb;
--muted: #aeb8c8;
--accent: #f9c846;
--accent-ink: #14120a;
}

* {
box-sizing: border-box;
}

body {
margin: 0;
min-height: 100vh;
background: var(--bg);
color: var(--text);
font-family:
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
sans-serif;
}

main {
display: grid;
min-height: 100vh;
place-items: center;
padding: 32px 16px;
}

section {
width: min(720px, 100%);
}

.brand {
display: inline-flex;
align-items: center;
gap: 10px;
color: var(--accent);
font-size: 14px;
font-weight: 700;
letter-spacing: 0;
margin-bottom: 22px;
}

.mark {
display: grid;
width: 28px;
height: 28px;
place-items: center;
border: 1px solid var(--accent);
border-radius: 6px;
}

h1 {
font-size: clamp(38px, 7vw, 76px);
line-height: 0.96;
letter-spacing: 0;
margin: 0 0 18px;
}

p {
color: var(--muted);
font-size: 18px;
line-height: 1.6;
margin: 0 0 28px;
max-width: 58ch;
}

form {
display: grid;
grid-template-columns: 1fr auto;
gap: 10px;
padding: 10px;
background: var(--panel);
border: 1px solid var(--line);
border-radius: 8px;
}

label {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}

input,
button,
a {
font: inherit;
}

input {
width: 100%;
min-width: 0;
height: 48px;
color: var(--text);
background: #090c12;
border: 1px solid transparent;
border-radius: 6px;
outline: none;
padding: 0 14px;
}

input:focus {
border-color: var(--accent);
}

button,
.skip {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 48px;
border-radius: 6px;
border: 0;
padding: 0 18px;
font-weight: 700;
text-decoration: none;
}

button {
background: var(--accent);
color: var(--accent-ink);
cursor: pointer;
}

.skip {
width: max-content;
margin-top: 14px;
color: var(--text);
background: transparent;
border: 1px solid var(--line);
}

@media (max-width: 640px) {
main {
place-items: start;
padding-top: 56px;
}

form {
grid-template-columns: 1fr;
}

button,
.skip {
width: 100%;
}
}
</style>
</head>
<body>
<main>
<section>
<div class="brand" aria-label="Tailcall">
<span class="mark">&gt;</span>
Tailcall
</div>
<h1>GraphQL Playground</h1>
<p>
Connect to a GraphQL endpoint without loading the full editor until you are ready.
</p>
<form id="playground-form" action="/playground/app/" method="get">
<label for="endpoint">GraphQL endpoint</label>
<input
id="endpoint"
name="u"
type="url"
inputmode="url"
autocomplete="url"
placeholder="https://api.example.com/graphql"
/>
<button type="submit">Open</button>
</form>
<a class="skip" href="/playground/app/">Open empty playground</a>
</section>
</main>
<script>
const params = new URLSearchParams(window.location.search)
const endpoint = params.get("u")

if (endpoint) {
window.location.replace(`/playground/app/?u=${encodeURIComponent(endpoint)}`)
}
</script>
</body>
</html>
Loading