- Open your node: shows a 'Connected' badge (+ board name) once the board is claimed. - Say hi: replaces the liveness-poll self-test with an actual round-trip to the agent on the team's own board — sends a greeting, shows the agent's reply, and only unlocks once it answers. New blocking /nodes/:teamId/say-hi route (greeting never flashes, so no hang risk) + bridge.sayHi + client sayHi(). The /prompt route stays fire-and-forget for the flash path. - Pick your domain already captures to the store and syncs to the server DB via useCollectiveSync -> pushTeam, available to later steps/judging. - Proceed gates on connected + agent-replied + domain. Tests updated + say-hi coverage (route + page). Co-Authored-By: Claude Opus 4.8 <[email protected]>
75 lines
2.3 KiB
TypeScript
75 lines
2.3 KiB
TypeScript
import { Link } from 'react-router-dom'
|
|
import { useSession } from '@/store/session'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
export interface OpenYourNodeProps {
|
|
/** 'hero' is the big primary CTA used on the setup page; 'inline' is a compact
|
|
* link for reuse inside later module pages. */
|
|
variant?: 'hero' | 'inline'
|
|
className?: string
|
|
}
|
|
|
|
/**
|
|
* Reusable "Open your node" call-to-action. Opens the board's LAN ZeroClaw
|
|
* dashboard (device.nodeUrl) in a new tab. When the board isn't claimed yet it
|
|
* shows an amber prompt back to team registration instead.
|
|
*/
|
|
export function OpenYourNode({ variant = 'hero', className }: OpenYourNodeProps) {
|
|
const device = useSession((s) => s.device)
|
|
const canOpen = device.connected && !!device.nodeUrl
|
|
|
|
if (!canOpen) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'border border-amber/40 bg-amber/5 rounded-md px-4 py-3 text-sm',
|
|
className,
|
|
)}
|
|
>
|
|
No node yet — <Link to="/workshop" className="underline font-medium">claim your board first</Link>.
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (variant === 'inline') {
|
|
return (
|
|
<a
|
|
href={device.nodeUrl!}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className={cn(
|
|
'inline-flex items-center gap-1 font-mono text-[11px] text-primary hover:underline tracking-widest uppercase',
|
|
className,
|
|
)}
|
|
>
|
|
Open your node →
|
|
</a>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<a
|
|
href={device.nodeUrl!}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className={cn(
|
|
'group flex items-center justify-between gap-4 rounded-lg border border-primary/40 bg-primary/5 px-6 py-5 transition-colors hover:bg-primary/10',
|
|
className,
|
|
)}
|
|
>
|
|
<div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-xl font-bold tracking-tight">Open your node →</span>
|
|
<span className="font-mono text-[9px] uppercase tracking-widest text-teal border border-teal/40 bg-teal/10 rounded px-1.5 py-0.5">
|
|
Connected
|
|
</span>
|
|
</div>
|
|
<div className="font-mono text-[10px] text-muted-foreground mt-1 truncate max-w-xs">
|
|
{device.port ? `${device.port} · ` : ''}{device.nodeUrl}
|
|
</div>
|
|
</div>
|
|
<span className="w-2.5 h-2.5 rounded-full bg-teal animate-pulse shrink-0" aria-hidden />
|
|
</a>
|
|
)
|
|
}
|