Add src/components/HostCard.tsx
This commit is contained in:
parent
7a6fdde3ed
commit
4579878886
1 changed files with 107 additions and 0 deletions
107
src/components/HostCard.tsx
Normal file
107
src/components/HostCard.tsx
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Host, api } from "../api";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
host: Host;
|
||||||
|
onRefresh: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HostCard({ host, onRefresh }: Props) {
|
||||||
|
const [connecting, setConnecting] = useState(false);
|
||||||
|
const [pairing, setPairing] = useState(false);
|
||||||
|
const [flash, setFlash] = useState<string | null>(null);
|
||||||
|
|
||||||
|
function showFlash(msg: string) {
|
||||||
|
setFlash(msg);
|
||||||
|
setTimeout(() => setFlash(null), 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleConnect() {
|
||||||
|
setConnecting(true);
|
||||||
|
try {
|
||||||
|
await api.connect(host.ip);
|
||||||
|
showFlash("Launching Moonlight…");
|
||||||
|
} catch (e: unknown) {
|
||||||
|
showFlash(e instanceof Error ? e.message : "Failed to connect");
|
||||||
|
} finally {
|
||||||
|
setConnecting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePair() {
|
||||||
|
setPairing(true);
|
||||||
|
try {
|
||||||
|
await api.pair(host.ip);
|
||||||
|
showFlash("Pairing dialog opened…");
|
||||||
|
onRefresh();
|
||||||
|
} catch (e: unknown) {
|
||||||
|
showFlash(e instanceof Error ? e.message : "Pairing failed");
|
||||||
|
} finally {
|
||||||
|
setPairing(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onlineDot = host.online ? "bg-green-400" : "bg-gray-500";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`
|
||||||
|
relative flex flex-col gap-3 p-5 rounded-xl border
|
||||||
|
bg-surface-raised
|
||||||
|
${host.online
|
||||||
|
? "border-surface-elevated hover:border-accent/60 cursor-pointer group"
|
||||||
|
: "border-surface-elevated opacity-60"
|
||||||
|
}
|
||||||
|
transition-all duration-200
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{/* Online indicator */}
|
||||||
|
<span className={`absolute top-4 right-4 w-2.5 h-2.5 rounded-full ${onlineDot}`} />
|
||||||
|
|
||||||
|
{/* Host icon placeholder */}
|
||||||
|
<div className="w-12 h-12 rounded-lg bg-surface-elevated flex items-center justify-center text-2xl">
|
||||||
|
🖥
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-white truncate">{host.name}</h3>
|
||||||
|
<p className="text-xs text-gray-400 mt-0.5">
|
||||||
|
{host.ip}:{host.port}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Flash message */}
|
||||||
|
{flash && (
|
||||||
|
<p className="text-xs text-accent animate-pulse">{flash}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex gap-2 mt-auto">
|
||||||
|
<button
|
||||||
|
onClick={handleConnect}
|
||||||
|
disabled={!host.online || connecting}
|
||||||
|
className="
|
||||||
|
flex-1 py-1.5 px-3 rounded-lg text-sm font-medium
|
||||||
|
bg-accent hover:bg-accent-hover
|
||||||
|
disabled:opacity-40 disabled:cursor-not-allowed
|
||||||
|
transition-colors duration-150
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{connecting ? "Launching…" : "Connect"}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handlePair}
|
||||||
|
disabled={!host.online || pairing}
|
||||||
|
className="
|
||||||
|
py-1.5 px-3 rounded-lg text-sm font-medium
|
||||||
|
bg-surface-elevated hover:bg-surface-elevated/80
|
||||||
|
disabled:opacity-40 disabled:cursor-not-allowed
|
||||||
|
transition-colors duration-150
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{pairing ? "…" : "Pair"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue