← Public packages
@kentcdodds/world-builder
Build a shared 3D voxel world with friends and your AI agents, live.
app/client/Landing.tsx
147 lines · 4.7 KB · TypeScriptimport { useEffect, useMemo, useState } from 'react'
import { planCastle } from '../../src/shared/castle.ts'
import { BotIcon, PersonIcon, SyncIcon } from './icons.tsx'
import { navigate, signInHref, signOutHref } from './nav.ts'
import type { BlockRow, BuildRow, PlayerRow } from './powersync.ts'
import { World } from './World.tsx'
const PREVIEW_WORLD = 'preview'
const noop = () => {}
/** A castle that builds itself in a loop: a local stand-in for an agent build streaming in. */
function usePreviewBlocks() {
const plan = useMemo(() => planCastle(0, 0), [])
const [count, setCount] = useState(0)
useEffect(() => {
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches
if (reduce) {
setCount(plan.length)
return
}
const t = setInterval(() => setCount((c) => (c >= plan.length + 60 ? 0 : c + 12)), 120)
return () => clearInterval(t)
}, [plan])
return useMemo<Array<BlockRow>>(
() =>
plan.slice(0, Math.min(count, plan.length)).map((b) => ({
id: `${b.x}:${b.y}:${b.z}`,
world_id: PREVIEW_WORLD,
x: b.x,
y: b.y,
z: b.z,
material: b.material,
build_id: null,
placed_by: null,
seq: null,
})),
[plan, count],
)
}
export function Landing({ appBase }: { appBase: string }) {
const blocks = usePreviewBlocks()
const [me, setMe] = useState<{ signedIn: boolean; displayName?: string } | null>(null)
const [creating, setCreating] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
void fetch(`${appBase}/api/me`, { credentials: 'same-origin' })
.then((r) => r.json())
.then(setMe, () => setMe({ signedIn: false }))
}, [appBase])
async function createRoom() {
setCreating(true)
setError(null)
try {
const res = await fetch(`${appBase}/api/rooms`, { method: 'POST', credentials: 'same-origin' })
const body = (await res.json().catch(() => ({}))) as { room?: { slug: string }; error?: string }
if (!res.ok || !body.room) throw new Error(body.error ?? 'Could not create a room. Try again.')
navigate(`/r/${body.room.slug}`)
} catch (e) {
setError(e instanceof Error ? e.message : 'Could not create a room. Try again.')
setCreating(false)
}
}
const signedOut = me !== null && !me.signedIn
return (
<div className="landing">
<div className="landing-preview" aria-hidden="true">
<World
blocks={blocks}
players={[] as Array<PlayerRow>}
builds={[] as Array<BuildRow>}
meId={null}
mode="place"
canEdit={false}
onPlace={noop}
onRemove={noop}
onHover={noop}
/>
</div>
<header className="landing-top">
<div className="brand">
<img src={`${document.documentElement.dataset.assetBase}/icon.png`} alt="" width={30} height={30} />
<strong>World Builder</strong>
</div>
{me?.signedIn ? (
<a className="chip" href={signOutHref()} title="Sign out of Kody on this site">
Sign out
</a>
) : signedOut ? (
<a className="chip" href={signInHref()}>
Sign in to build
</a>
) : null}
</header>
<main className="hero">
<p className="eyebrow">
<span className="live-dot" /> Live multiplayer · humans + agents
</p>
<h1>Build a 3D world together, with your friends and your AI agents.</h1>
<p className="lede">
Open a room, share the link, and place blocks together. Then paste one prompt into Claude, ChatGPT or Grok and
ask for a castle. Everyone in the room watches it go up block by block.
</p>
<div className="hero-actions">
<button type="button" className="cta" onClick={createRoom} disabled={creating || signedOut || me === null}>
{creating ? 'Creating…' : 'Create a room'}
</button>
<button type="button" className="cta ghost" onClick={() => navigate('/r/main')}>
Watch the main world
</button>
{signedOut ? <p className="hint"><a href={signInHref()}>Sign in to build</a> and create your own room, or watch the main world.</p> : null}
{error ? (
<p className="hint error" role="alert">
{error}
</p>
) : null}
</div>
<ul className="features">
<li>
<PersonIcon />
<span>
<strong>One-click rooms</strong> with a private share link
</span>
</li>
<li>
<BotIcon />
<span>
<strong>Bring your own agent</strong> over Kody MCP or the CLI
</span>
</li>
<li>
<SyncIcon state="synced" />
<span>
<strong>Live sync</strong> for every block, powered by PowerSync
</span>
</li>
</ul>
</main>
<footer className="landing-foot">
Powered by <a href="https://www.powersync.com" target="_blank" rel="noreferrer">PowerSync</a> and{' '}
<a href="https://kody.codes" target="_blank" rel="noreferrer">Kody</a>
</footer>
</div>
)
}