Revamp UI, enhance documentation, and integrate newsletter functionality - #53
Revamp UI, enhance documentation, and integrate newsletter functionality#53smilewithkhushi wants to merge 32 commits into
Conversation
…update asset ID references
Enhance Stork oracle and MCP Server documentation
- Implemented a new Newsletter component in index.tsx that handles user input for name and email. - Added form submission logic to send subscription requests to a new API endpoint. - Created a new API route (subscribe.ts) to handle subscription requests and interact with the Beehiiv API. - Included error handling and loading states for better user experience. - Updated package.json to include @vercel/node for serverless function support.
feat: add newsletter subscription functionality with API integration
refractor: clean up
- Added Tailwind CSS and PostCSS dependencies in package.json. - Updated custom.css to include Tailwind's theme and utilities, and adjusted dark mode styles. - Refactored index.module.css to improve layout and styling, including adjustments to hero and section components. - Enhanced index.tsx with new building cards and improved layout for hero and sections. - Redesigned the footer component to remove CSS module dependency and implement Tailwind styles. - Updated Navbar styles for better visual consistency and responsiveness. - Created a new Tailwind plugin for PostCSS integration.
… styles, and update what-is-horizen content
…and AI discoverability fixes
…update asset ID references
- Implemented a new Newsletter component in index.tsx that handles user input for name and email. - Added form submission logic to send subscription requests to a new API endpoint. - Created a new API route (subscribe.ts) to handle subscription requests and interact with the Beehiiv API. - Included error handling and loading states for better user experience. - Updated package.json to include @vercel/node for serverless function support.
Rewrote and expanded Vela docs with new Developer Reference pages, fixed terminology, removed Hello World references, and updated LLM index files. Added newsletter Cloudflare function, migration section to llms-ctx, and minor footer and landing page updates.
Revamp UI, enhance documentation, and integrate AI tools
cronicc
left a comment
There was a problem hiding this comment.
Thanks for this. The Vela restructure and the Stork fixes are solid: the keccak-derived asset IDs and the uint64/int192 struct and ABI shapes were checked and are correct, the governance link fixes in llms-ctx.txt match the real filenames, and a local build passes with no broken-link warnings.
Two things need fixing before merge, plus smaller items inline.
Must fix
- MCP page says the mainnet chain ID is
7332. It is26514. (inline) public/llms-ctx.txtroadmap link 404s. The built route is/vela/roadmap. (inline)
Should fix (all inline)
- Newsletter function:
first_nameis not a beehiiv field, no bot protection (Turnstile requested inline, buildable against Cloudflare's public test keys with no dashboard access), no email validation, unguardedrequest.json(), missing@cloudflare/workers-typessonpm run typecheckregresses, and_routes.jsonis in a location Cloudflare ignores. - Committed
public/llms.txtandpublic/llms-full.txtare overwritten by the plugin at build time and are already stale. Please drop them. horizen-mcpnpm package ownership, and the Claude config paths on the MCP page.
Not anchored to a line
- Redirects:
/vela/getting-started/hello-worldand/vela/limitations/limitationsare removed with nothing pointing at the new pages, so existing links and search results will 404. Please add@docusaurus/plugin-client-redirectswithhello-world -> /vela/getting-started/first-confidential-appandlimitations/limitations -> /vela/roadmap. - Deploy dependency (maintainer side, not on you): the function reads
BEEHIIV_API_KEY,PUBLICATION_ID, andTURNSTILE_SECRET_KEYfrom the Pages environment, and the build readsTURNSTILE_SITE_KEY. We will create the Turnstile widget (hostnamesdocs.horizen.ioandhorizen-2-docs.pages.dev) and set all four variables in the Pages project before merge. Fork PRs get no preview deploy, so the live endpoint can only be checked after merge:curl -X POST -H 'content-type: application/json' -d '{}' https://docs.horizen.io/api/subscribeshould return a 4xx JSON error from the function (today it returns 405 from static hosting). - The Vela product claims (v0.2.0 shipped ERC-20 and deanonymization, Base Sepolia availability for early builders, AWS Nitro, Anvil account #0 holding
DEPLOYER_ROLE) need a confirmation from the Vela team before merge.
|
|
||
| | Editor | Config file path | | ||
| |---|---| | ||
| | Claude Code / Claude Desktop | `~/.claude/claude_desktop_config.json` | |
There was a problem hiding this comment.
Neither half of this row is right for Claude. Claude Desktop reads claude_desktop_config.json from ~/Library/Application Support/Claude/ on macOS or %APPDATA%\Claude\ on Windows. Claude Code does not use that file at all; it is claude mcp add horizen -- npx -y horizen-mcp, or a project-level .mcp.json. Suggest splitting into two rows.
| ## Quickstart | ||
|
|
||
| ```bash | ||
| npx -y horizen-mcp |
There was a problem hiding this comment.
The horizen-mcp package on npm is currently published from a personal account rather than a Horizen org account. Before official docs tell users to run npx -y <pkg>, ownership should move under org control (a scoped name like @horizen/mcp would make that visible).
There was a problem hiding this comment.
I'm initiating the horizen mcp repo transfer to the Horizen github account
| <p className="text-[#030E24] font-semibold text-lg m-0">You're subscribed! Check your inbox.</p> | ||
| </div> | ||
| ) : ( | ||
| <form onSubmit={handleSubmit} className="flex items-center gap-10 flex-1 justify-end max-[860px]:w-full max-[860px]:flex-col max-[860px]:items-stretch max-[860px]:gap-5"> |
There was a problem hiding this comment.
Frontend half of the Turnstile ask (see the comment on functions/api/subscribe.ts for the server side). Drop-in replacement for the Newsletter component, plus the declare global and script loader above it:
// imports at the top of src/pages/index.tsx become:
// import React, { useEffect, useRef, useState } from 'react';
declare global {
interface Window {
turnstile?: {
render: (el: HTMLElement, opts: Record<string, unknown>) => string;
reset: (id?: string) => void;
remove: (id: string) => void;
};
}
}
const TURNSTILE_SRC = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';
function loadTurnstile(): Promise<void> {
return new Promise((resolve, reject) => {
if (window.turnstile) return resolve();
const existing = document.querySelector<HTMLScriptElement>(`script[src="${TURNSTILE_SRC}"]`);
if (existing) {
existing.addEventListener('load', () => resolve(), { once: true });
return;
}
const s = document.createElement('script');
s.src = TURNSTILE_SRC;
s.async = true;
s.onload = () => resolve();
s.onerror = () => reject(new Error('Turnstile failed to load'));
document.head.appendChild(s);
});
}
/* ─── Newsletter ────────────────────────────────────────────────────────── */
function Newsletter() {
const { siteConfig } = useDocusaurusContext();
const siteKey = siteConfig.customFields?.turnstileSiteKey as string;
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [token, setToken] = useState('');
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
const [errorMsg, setErrorMsg] = useState('');
const widgetRef = useRef<HTMLDivElement>(null);
const widgetId = useRef<string | null>(null);
// Runs client-side only: Docusaurus pre-renders this page at build time.
useEffect(() => {
let cancelled = false;
loadTurnstile()
.then(() => {
if (cancelled || !widgetRef.current || !window.turnstile) return;
widgetId.current = window.turnstile.render(widgetRef.current, {
sitekey: siteKey,
appearance: 'interaction-only', // invisible unless a challenge is actually needed
theme: 'light',
callback: (t: string) => setToken(t),
'expired-callback': () => setToken(''),
'error-callback': () => setToken(''),
});
})
.catch(() => {
setErrorMsg('Could not load verification. Please disable content blockers and reload.');
setStatus('error');
});
return () => {
cancelled = true;
if (widgetId.current && window.turnstile) window.turnstile.remove(widgetId.current);
};
}, [siteKey]);
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setStatus('loading');
setErrorMsg('');
const website = (new FormData(e.currentTarget).get('website') as string | null) ?? '';
try {
const res = await fetch('/api/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, name, turnstileToken: token, website }),
});
if (res.ok) {
setStatus('success');
setName('');
setEmail('');
} else {
const data = (await res.json().catch(() => ({}))) as { error?: string };
setErrorMsg(data.error || 'Something went wrong. Please try again.');
setStatus('error');
}
} catch {
setErrorMsg('Something went wrong. Please try again.');
setStatus('error');
} finally {
// Tokens are single-use: fetch a fresh one for the next attempt.
if (widgetId.current && window.turnstile) window.turnstile.reset(widgetId.current);
setToken('');
}
}
const inputClass =
'bg-transparent border-0 border-b border-[#030E24] text-[#030E24] placeholder:text-[#030E24] placeholder:font-medium text-base outline-none w-60 py-2 rounded-none shadow-none focus:border-b-2 max-[860px]:w-full';
return (
<section className="w-full min-h-52 bg-[rgba(254,203,23,1)] flex items-center px-25 py-12.5 max-[1100px]:px-10 max-[860px]:px-6 max-[860px]:py-10">
<div className="flex items-center justify-between gap-17.75 w-full max-[860px]:flex-col max-[860px]:items-start max-[860px]:gap-6">
<span className="text-[#030E24] font-extrabold text-[clamp(1.75rem,2.4vw,2.5rem)] whitespace-nowrap leading-[1.1] tracking-tight shrink-0" style={{ fontFamily: "'Funnel Display', sans-serif" }}>
Sign Up for Newsletter
</span>
{status === 'success' ? (
<div className="flex-1 flex justify-end max-[860px]:justify-start">
<p className="text-[#030E24] font-semibold text-lg m-0">You're subscribed! Check your inbox.</p>
</div>
) : (
<form onSubmit={handleSubmit} className="flex items-center gap-10 flex-1 justify-end max-[860px]:w-full max-[860px]:flex-col max-[860px]:items-stretch max-[860px]:gap-5">
<input type="text" placeholder="Name" value={name} onChange={e => setName(e.target.value)} className={inputClass} aria-label="Name" />
<input type="email" placeholder="Email Address" value={email} onChange={e => setEmail(e.target.value)} required className={inputClass} aria-label="Email address" />
{/* Honeypot: off-screen rather than display:none, since some bots skip hidden inputs. */}
<input
type="text"
name="website"
tabIndex={-1}
autoComplete="off"
aria-hidden="true"
style={{ position: 'absolute', left: '-9999px', width: 1, height: 1, opacity: 0 }}
/>
<div className="flex flex-col items-end gap-1 max-[860px]:items-stretch">
{status === 'error' && (
<p role="alert" className="text-red-700 text-xs m-0 text-right max-[860px]:text-left">{errorMsg}</p>
)}
{/* Turnstile mounts here. Zero size unless a challenge is shown. */}
<div ref={widgetRef} />
<button
type="submit"
disabled={status === 'loading' || !token}
title={!token ? 'Verifying…' : undefined}
className="bg-white text-[#030E24] font-bold text-base px-10 py-4 rounded-full whitespace-nowrap min-w-40 cursor-pointer hover:bg-gray-50 border-none hover:shadow-md transition-all max-[860px]:w-full disabled:opacity-60 disabled:cursor-not-allowed"
>
{status === 'loading' ? 'Subscribing…' : 'Subscribe'}
</button>
</div>
</form>
)}
</div>
</section>
);
}Two small changes elsewhere:
src/pages/index.tsx imports:
import React, { useEffect, useRef, useState } from 'react';docusaurus.config.ts, top level of the config object:
customFields: {
turnstileSiteKey: process.env.TURNSTILE_SITE_KEY ?? '1x00000000000000000000AA',
},The fallback is Cloudflare's always-pass test site key, so local builds and wrangler pages dev need no setup. In production TURNSTILE_SITE_KEY comes from the Pages build environment.
Notes on behaviour: the widget renders interaction-only, so it takes no space unless Cloudflare decides a challenge is needed. The button stays disabled until a token arrives (usually under a second), and every response resets the widget because tokens are single-use. If the script is blocked by a content blocker, the form shows an explanatory error instead of silently failing.
| cp .dev.vars.example .dev.vars | ||
| ``` | ||
|
|
||
| `.dev.vars` is gitignored and never committed. Get the real values from the Cloudflare Pages dashboard under **Settings → Environment Variables**. |
There was a problem hiding this comment.
Once Turnstile is in, this becomes four variables: BEEHIIV_API_KEY, PUBLICATION_ID, TURNSTILE_SECRET_KEY (runtime, encrypted) and TURNSTILE_SITE_KEY (build time, plain). Worth noting here that they also have to be set in the Pages project's Production environment (and Preview, if wanted); without them every submit fails. For local dev, point readers at Cloudflare's always-pass test keys so no real widget is needed.
No description provided.