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
46 changes: 46 additions & 0 deletions apps/web/components/code-block.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { FC, useCallback, useState } from 'react'
import styles from 'styles/CodeBlock.module.css'

interface Props {
code: string
label: string
}

/**
* A copyable snippet. The examples on /keys exist to be pasted into a shell or
* an agent's tool call, and they rendered as unstyled monospace with no
* boundary, so it was not obvious where a snippet started or ended.
*
* Long lines scroll inside the block instead of wrapping: a wrapped curl line
* or a status URL broken mid-token reads as two commands.
*/
export const CodeBlock: FC<Props> = ({ code, label }) => {
const [copied, setCopied] = useState(false)

const copy = useCallback(async () => {
try {
await navigator.clipboard.writeText(code)
setCopied(true)
setTimeout(() => setCopied(false), 1500)
} catch {
// Clipboard can be unavailable (insecure context, denied permission).
// The text is still selectable, so there is nothing else to do.
}
}, [code])

return (
<div className={styles.block}>
<pre className={styles.pre}>
<code>{code}</code>
</pre>
<button
type="button"
className={styles.copy}
onClick={copy}
aria-label={`Copy ${label}`}
>
{copied ? 'Copied' : 'Copy'}
</button>
</div>
)
}
75 changes: 50 additions & 25 deletions apps/web/components/faucet-header.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Link from 'next/link'
import { useRouter } from 'next/router'
import { FC } from 'react'
import { GitHubAuth } from 'components/github-auth'
import { Logo } from 'components/logo'
Expand All @@ -12,40 +13,64 @@ interface Props {
isOutOfCELO: boolean
}

export const FaucetHeader: FC<Props> = ({ network, isOutOfCELO }) => (
<div className={styles.top}>
{isOutOfCELO && (
<header className={styles.notice}>
<span>The Faucet is out of CELO for now.</span>
</header>
)}
<div className={`${styles.topBar}`}>
{/*
/**
* The two pages a visitor moves between. /keys used to offer only the logo as
* a way back, so "Faucet" sits next to "For Agents" and the current page is
* marked with aria-current rather than by colour alone.
*/
const navLinkClass = `${inter.className} ${styles.navLink}`

export const FaucetHeader: FC<Props> = ({ network, isOutOfCELO }) => {
const { pathname } = useRouter()
const onFaucet = pathname === '/[chain]'
const onKeys = pathname === '/keys'

return (
<div className={styles.top}>
{isOutOfCELO && (
<header className={styles.notice}>
<span>The Faucet is out of CELO for now.</span>
</header>
)}
<div className={`${styles.topBar}`}>
{/*
The logo was inert on every page, so /keys, /signin and /auth-error
had no way back to the faucet at all.
*/}
{/*
{/*
Uses the network it was handed rather than a constant: pages that are
not network-scoped pass celo-sepolia explicitly, and a second chain
would otherwise send its own logo to the wrong faucet.
*/}
<Link
href={`/${network}`}
aria-label="Celo faucet home"
className={`${styles.logo} dark:filter-[invert(1)]`}
>
<Logo />
</Link>
<div className="flex flex-row items-center gap-3 pr-[40px]">
<Link
href="/keys"
className={`${inter.className} text-sm underline underline-offset-4`}
href={`/${network}`}
aria-label="Celo faucet home"
className={`${styles.logo} dark:filter-[invert(1)]`}
>
For Agents
<Logo />
</Link>
<GitHubAuth />
<ModeToggle />

<nav aria-label="Main" className={styles.nav}>
<Link
href={`/${network}`}
className={navLinkClass}
aria-current={onFaucet ? 'page' : undefined}
>
Faucet
</Link>
<Link
href="/keys"
className={navLinkClass}
aria-current={onKeys ? 'page' : undefined}
>
For Agents
</Link>
</nav>
<div className="flex flex-row items-center gap-3 pr-3 sm:pr-[40px]">
<GitHubAuth />
<ModeToggle />
</div>
</div>
</div>
</div>
)
)
}
8 changes: 6 additions & 2 deletions apps/web/pages/[chain].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,11 @@ const Home: NextPage<Props> = ({ isOutOfCELO, network }: Props) => {
/>
<main className={styles.main}>
<FaucetHeader network={network} isOutOfCELO={isOutOfCELO} />
<Card className="w-full max-w-sm items-stretch">
{/*
Same width as the /keys card, so moving between the two pages does
not resize the main card.
*/}
<Card className="w-full max-w-lg items-stretch">
<CardHeader>
{/*
A real h1 rather than CardTitle: that renders a div, so both
Expand All @@ -125,7 +129,7 @@ const Home: NextPage<Props> = ({ isOutOfCELO, network }: Props) => {
<CardContent>
<RequestForm network={network} isOutOfCELO={isOutOfCELO} />
</CardContent>
<CardFooter className="flex-col gap-2">
<CardFooter className="flex-col items-start gap-2">
<div className="mt-4 text-sm flex flex-col gap-0.5 items-start">
{!session && (
<>
Expand Down
41 changes: 26 additions & 15 deletions apps/web/pages/keys.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,24 @@ import {
} from '../@/components/ui/card'
import { Input } from '../@/components/ui/input'
import { Label } from '../@/components/ui/label'
import { CodeBlock } from 'components/code-block'
import { FaucetHeader } from 'components/faucet-header'
import { JsonLd } from 'components/json-ld'
import { Seo } from 'components/seo'
import { DAILY_REQUESTS, SITE_URL } from 'config/site'
import styles from 'styles/Home.module.css'
import { ApiKeyRecord, KEY_TTL_DAYS, MAX_KEYS_PER_OWNER } from 'types'
import { ApiKeyRecord, KEY_TTL_DAYS, MAX_KEYS_PER_OWNER, Network } from 'types'
import { inter } from 'utils/inter'

const formatDate = (ms: number) => new Date(ms).toISOString().slice(0, 10)

/**
* Keys only work on Celo Sepolia (see Limits below), so this page belongs to
* that faucet. One constant feeds the header and the footer's way back, which
* used to be two separate hardcoded strings.
*/
const KEYS_NETWORK: Network = 'celo-sepolia'

const ApiKeys: NextPage = () => {
const { data: session } = useSession()
const [keys, setKeys] = useState<ApiKeyRecord[]>([])
Expand Down Expand Up @@ -143,7 +151,7 @@ const ApiKeys: NextPage = () => {
}}
/>
<main className={styles.main}>
<FaucetHeader network="celo-sepolia" isOutOfCELO={false} />
<FaucetHeader network={KEYS_NETWORK} isOutOfCELO={false} />

<Card className="w-full max-w-lg items-stretch">
<CardHeader>
Expand Down Expand Up @@ -278,7 +286,7 @@ const ApiKeys: NextPage = () => {
)}
</CardContent>

<CardFooter className="flex-col gap-2">
<CardFooter className="flex-col items-start gap-2">
<div className="mt-4 text-sm flex flex-col gap-0.5 items-start">
<small className={inter.className}>
&bull; You may hold {MAX_KEYS_PER_OWNER} keys. They expire after{' '}
Expand All @@ -290,7 +298,7 @@ const ApiKeys: NextPage = () => {
</small>
<small className={inter.className}>
&bull;{' '}
<Link className="underline" href="/celo-sepolia">
<Link className="underline" href={`/${KEYS_NETWORK}`}>
Back to the faucet
</Link>
</small>
Expand All @@ -310,11 +318,12 @@ const ApiKeys: NextPage = () => {
No browser, no GitHub. Sign a challenge with the wallet
registered for your ERC-8004 agent on Celo mainnet:
</p>
<pre className="overflow-x-auto text-xs">
<code>{`POST /api/keys/challenge {"agentId":"2"}
<CodeBlock
label="agent key requests"
code={`POST /api/keys/challenge {"agentId":"2"}
# sign the returned message, then
POST /api/keys/agent {"agentId":"2","nonce":"…","signature":"0x…"}`}</code>
</pre>
POST /api/keys/agent {"agentId":"2","nonce":"…","signature":"0x…"}`}
/>
<p className={inter.className}>
&bull; The key is bound to the agent, so it draws on its own
allowance rather than a person&apos;s: 2 requests a day for an
Expand Down Expand Up @@ -346,12 +355,13 @@ POST /api/keys/agent {"agentId":"2","nonce":"…","signature":"0x…"}`}</c
the entire point of publishing it for agents — sent a malformed
beneficiary and got a 400 back.
*/}
<pre className="overflow-x-auto text-xs">
<code>{`curl -X POST https://faucet.celo.org/api/faucet \\
<CodeBlock
label="curl example"
code={`curl -X POST https://faucet.celo.org/api/faucet \\
-H "Authorization: Bearer $CELO_FAUCET_API_KEY" \\
-H 'Content-Type: application/json' \\
-d '{"beneficiary":"0xYOUR_ADDRESS","network":"celo-sepolia"}'`}</code>
</pre>
-d '{"beneficiary":"0xYOUR_ADDRESS","network":"celo-sepolia"}'`}
/>
</div>
</Card>

Expand Down Expand Up @@ -423,9 +433,10 @@ POST /api/keys/agent {"agentId":"2","nonce":"…","signature":"0x…"}`}</c
<p className={inter.className}>
A request returns a key. Poll it for the transaction hash:
</p>
<pre className="whitespace-pre-wrap break-all text-xs">
<code>{`GET /api/status?key=<key>&network=celo-sepolia`}</code>
</pre>
<CodeBlock
label="status request"
code={`GET /api/status?key=<key>&network=celo-sepolia`}
/>
</div>
</Card>

Expand Down
40 changes: 40 additions & 0 deletions apps/web/styles/CodeBlock.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
.block {
position: relative;
min-width: 0;
max-width: 100%;
margin: 0.25rem 0;
}

.pre {
margin: 0;
/*
The copy button sits in the top padding, above the first line, so code
scrolled sideways on a phone never runs underneath it.
*/
padding: 2.25rem 1rem 0.75rem;
overflow-x: auto;
white-space: pre;
font-size: 0.75rem;
line-height: 1.6;
background: var(--secondary-background);
border: 2px solid var(--border);
border-radius: 5px;
}

.copy {
position: absolute;
top: 0.5rem;
right: 0.5rem;
padding: 0.125rem 0.5rem;
font-size: 0.75rem;
font-weight: 600;
color: var(--main-foreground);
background: var(--main);
border: 2px solid var(--border);
border-radius: 5px;
}

.copy:focus-visible {
outline: 2px solid var(--ring);
outline-offset: 2px;
}
39 changes: 39 additions & 0 deletions apps/web/styles/FaucetHeader.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,42 @@

.topBar {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
}

.nav {
display: flex;
align-items: center;
gap: 0.75rem;
margin-left: auto;
margin-right: 0.75rem;
}

.logo {
display: inline-block;
margin-top: 20px;
margin-left: 20px;
}

.navLink {
font-size: 0.875rem;
white-space: nowrap;
text-decoration: underline;
text-underline-offset: 4px;
padding: 0.125rem 0.25rem;
border-radius: 4px;
}

/* The current page reads as a place, not a link: highlighted, no underline. */
.navLink[aria-current='page'] {
text-decoration: none;
font-weight: 700;
background: var(--main);
color: var(--main-foreground);
}

.notice {
position: relative;
left: 0;
Expand All @@ -32,8 +58,21 @@
}
.logo {
margin-top: 4px;
/* Two nav links plus sign-in and the toggle have to fit in 375px. */
margin-left: 12px;
}
.logo svg {
width: 70px;
}
/*
Two nav links, sign-in and the theme toggle do not fit beside the logo
in 375px, so the nav takes its own row under the bar.
*/
.nav {
order: 3;
width: 100%;
justify-content: flex-end;
margin: 0;
padding: 0.25rem 12px 0.5rem;
}
}
Loading
Loading