111 lines
4.8 KiB
JavaScript
111 lines
4.8 KiB
JavaScript
import { motion } from "framer-motion";
|
|
import { ArrowUpRight, Pencil, Server, Trash2 } from "lucide-react";
|
|
import CopyButton from "./ui/CopyButton";
|
|
import Button from "./ui/Button";
|
|
|
|
export default function ServerCard({
|
|
server,
|
|
index = 0,
|
|
selected = false,
|
|
onSelectServer,
|
|
onOpenServer,
|
|
onEditServer,
|
|
onDeleteServer,
|
|
}) {
|
|
return (
|
|
<motion.article
|
|
layout
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ duration: 0.18, delay: Math.min(index * 0.03, 0.3) }}
|
|
className={`card group relative overflow-hidden transition hover:border-line-strong hover:shadow-xl hover:shadow-black/25 ${
|
|
selected ? "border-accent shadow-lg shadow-cyan-500/10" : ""
|
|
}`}
|
|
>
|
|
<div className="pointer-events-none absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-cyan-500/50 to-transparent opacity-0 transition group-hover:opacity-100" />
|
|
|
|
<div className="flex flex-col gap-3 border-b border-line p-4 sm:flex-row sm:items-center sm:justify-between">
|
|
<button
|
|
type="button"
|
|
onClick={() => onOpenServer(server)}
|
|
className="group/head -m-1 flex min-w-0 items-center gap-3 rounded-xl p-1 text-left"
|
|
>
|
|
<span className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-surface-2 text-accent transition group-hover/head:bg-gradient-to-br group-hover/head:from-cyan-500/20 group-hover/head:to-indigo-500/20">
|
|
<Server size={20} />
|
|
</span>
|
|
<span className="min-w-0 flex-1">
|
|
<span className="flex items-center gap-1.5">
|
|
<span className="truncate font-bold text-ink">{server.name}</span>
|
|
<ArrowUpRight size={14} className="shrink-0 text-accent opacity-0 transition group-hover/head:opacity-100" />
|
|
</span>
|
|
<span className="mono mt-0.5 flex items-center gap-1 text-xs text-ink-muted">
|
|
{server.ipAddress}
|
|
<CopyButton value={server.ipAddress} className="opacity-0 transition group-hover:opacity-100" />
|
|
</span>
|
|
</span>
|
|
</button>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<label className="flex items-center gap-2 rounded-lg border border-line bg-surface-2 px-2.5 py-1 text-xs font-semibold text-ink-soft transition hover:border-line-strong hover:text-ink">
|
|
<input
|
|
type="checkbox"
|
|
checked={selected}
|
|
onChange={(event) => onSelectServer(server, event.target.checked)}
|
|
className="h-4 w-4 accent-cyan-500"
|
|
/>
|
|
Select
|
|
</label>
|
|
{server.proxyPort && (
|
|
<span className="mono hidden rounded-lg bg-cyan-500/10 px-2.5 py-1 text-[11px] font-medium text-cyan-400 md:inline-block">
|
|
gateway :{server.proxyPort}
|
|
{server.proxyPath || "/api/nginx/app"}
|
|
</span>
|
|
)}
|
|
<span className="rounded-lg bg-surface-2 px-2.5 py-1 text-xs font-semibold text-ink-soft">
|
|
{server.services.length} {server.services.length === 1 ? "service" : "services"}
|
|
</span>
|
|
<button
|
|
aria-label={`Edit ${server.name}`}
|
|
className="rounded-lg p-2 text-ink-muted transition hover:bg-surface-2 hover:text-ink"
|
|
onClick={() => onEditServer(server)}
|
|
type="button"
|
|
>
|
|
<Pencil size={15} />
|
|
</button>
|
|
<button
|
|
aria-label={`Delete ${server.name}`}
|
|
className="rounded-lg p-2 text-ink-muted transition hover:bg-rose-500/10 hover:text-rose-400"
|
|
onClick={() => onDeleteServer(server)}
|
|
type="button"
|
|
>
|
|
<Trash2 size={15} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{server.description && <p className="border-b border-line px-4 py-2.5 text-xs leading-relaxed text-ink-soft">{server.description}</p>}
|
|
|
|
<div className="flex flex-wrap items-center justify-between gap-3 p-4">
|
|
<div className="flex min-w-0 flex-wrap gap-1.5">
|
|
{server.services.slice(0, 3).map((service) => (
|
|
<span key={service.id} className="mono max-w-56 truncate rounded-lg border border-line bg-surface-2/60 px-2 py-1 text-[11px] text-ink-soft">
|
|
{service.name}:{service.port}
|
|
</span>
|
|
))}
|
|
{server.services.length === 0 && (
|
|
<span className="text-[11px] text-ink-muted">No services yet</span>
|
|
)}
|
|
{server.services.length > 3 && (
|
|
<span className="mono rounded-lg bg-surface-3/60 px-2 py-1 text-[11px] font-semibold text-ink-muted">
|
|
+{server.services.length - 3}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<Button variant="secondary" size="sm" onClick={() => onOpenServer(server)}>
|
|
View services
|
|
</Button>
|
|
</div>
|
|
</motion.article>
|
|
);
|
|
}
|