mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-04 05:41:29 +00:00
123 lines
5.1 KiB
TypeScript
123 lines
5.1 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
|
|
import { scanDevices, pairDevice, type DiscoveredDevice } from '@/lib/api'
|
|
|
|
export function DeviceSearchModal({ onClose, onPaired }: { onClose: () => void; onPaired: () => void }) {
|
|
const [scanning, setScanning] = useState(true)
|
|
const [found, setFound] = useState<DiscoveredDevice[]>([])
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [pairing, setPairing] = useState<string | null>(null)
|
|
const [status, setStatus] = useState<string | null>(null)
|
|
|
|
const scan = async () => {
|
|
setScanning(true)
|
|
setError(null)
|
|
setStatus(null)
|
|
try {
|
|
setFound(await scanDevices())
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : String(e))
|
|
} finally {
|
|
setScanning(false)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
void scan()
|
|
}, [])
|
|
|
|
const connect = async (d: DiscoveredDevice) => {
|
|
setPairing(d.fingerprint)
|
|
setError(null)
|
|
setStatus(`Confirm the code ${d.code} on "${d.name}", then approve there. Waiting...`)
|
|
try {
|
|
const r = await pairDevice(d)
|
|
if (r.ok) {
|
|
setStatus(`Connected to "${r.name ?? d.name}".`)
|
|
onPaired()
|
|
setTimeout(onClose, 700)
|
|
} else {
|
|
setError(r.error ?? 'Pairing failed')
|
|
setStatus(null)
|
|
setPairing(null)
|
|
}
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : String(e))
|
|
setStatus(null)
|
|
setPairing(null)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/30 p-4" onClick={onClose}>
|
|
<div
|
|
className="w-full max-w-md overflow-hidden rounded-lg border border-border bg-card shadow-[0_24px_60px_-20px_rgba(0,0,0,0.35)]"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<div className="flex items-center justify-between border-b border-border px-5 py-3.5">
|
|
<h2 className="text-sm font-semibold text-foreground">Search local devices</h2>
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={() => void scan()}
|
|
disabled={scanning}
|
|
className="rounded-md border border-border px-2.5 py-1 text-xs text-tertiary-foreground transition-colors hover:text-foreground disabled:opacity-50"
|
|
>
|
|
Rescan
|
|
</button>
|
|
<button onClick={onClose} className="rounded-md px-2 py-1 text-tertiary-foreground hover:text-foreground" aria-label="Close">
|
|
✕
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="px-5 py-4">
|
|
{scanning ? (
|
|
<div className="flex items-center gap-3 py-6 text-sm text-tertiary-foreground">
|
|
<span className="h-4 w-4 animate-spin rounded-full border-2 border-border border-t-primary" />
|
|
Looking for devices on your network...
|
|
</div>
|
|
) : found.length === 0 ? (
|
|
<p className="py-6 text-center text-sm text-tertiary-foreground">
|
|
No devices found. On your other Mac run <span className="font-mono text-foreground">codeburn share</span> on the same Wi-Fi.
|
|
</p>
|
|
) : (
|
|
<div className="flex flex-col gap-2">
|
|
{found.map((d) => (
|
|
<div key={d.fingerprint} className="flex items-center gap-3 rounded-md border border-border px-3.5 py-3">
|
|
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-interactive-secondary text-primary">
|
|
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
|
|
<rect x="3" y="4" width="18" height="12" rx="2" />
|
|
<path d="M8 20h8M12 16v4" />
|
|
</svg>
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<div className="truncate text-sm font-medium text-foreground">{d.name}</div>
|
|
<div className="truncate font-mono text-xs text-tertiary-foreground">
|
|
{d.host}:{d.port}
|
|
</div>
|
|
</div>
|
|
{d.paired ? (
|
|
<span className="rounded-full bg-primary/10 px-2.5 py-1 text-xs font-medium text-primary">Connected</span>
|
|
) : pairing === d.fingerprint ? (
|
|
<span className="font-mono text-xs text-tertiary-foreground">code {d.code}</span>
|
|
) : (
|
|
<button
|
|
onClick={() => void connect(d)}
|
|
disabled={!!pairing}
|
|
className="rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground transition-opacity hover:opacity-90 disabled:opacity-50"
|
|
>
|
|
Connect
|
|
</button>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{status && <p className="mt-3 text-xs text-tertiary-foreground">{status}</p>}
|
|
{error && <p className="mt-3 text-xs text-chart-8">{error}</p>}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|