Files
apress/src/components/OpenYourNode.tsx
T
Omar SobhandClaude Opus 4.8 cefddb245c feat(phase2): real say-hi chat + Connected node state on Meet-your-node
- 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]>
2026-07-21 09:26:50 -07:00

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>
)
}