Import source: web/src/components/TargetCard
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -0,0 +1,82 @@
|
|||||||
|
import { memo } from "react";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
import type { LiveTarget } from "../lib/types";
|
||||||
|
import { StatusRing } from "./StatusRing";
|
||||||
|
import { fmtInterval } from "../lib/format";
|
||||||
|
import { springs, STATE_COLOR, useUI } from "../lib/ui";
|
||||||
|
|
||||||
|
interface TargetCardProps {
|
||||||
|
target: LiveTarget;
|
||||||
|
index: number;
|
||||||
|
onSelect: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TargetCard = memo(function TargetCard({
|
||||||
|
target,
|
||||||
|
index,
|
||||||
|
onSelect,
|
||||||
|
}: TargetCardProps) {
|
||||||
|
const { t, lang, window, reduced } = useUI();
|
||||||
|
const uptime = window === "7d" ? target.uptime_7d : target.uptime_24h;
|
||||||
|
const delay = 0.1 + Math.min(index, 12) * 0.045;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.button
|
||||||
|
layoutId={`card-${target.id}`}
|
||||||
|
onClick={() => onSelect(target.id)}
|
||||||
|
className="group relative flex flex-col items-center text-center px-4 pt-5 pb-4 rounded-md border border-ink-800 bg-ink-900/35 cursor-pointer overflow-hidden"
|
||||||
|
initial={reduced ? false : { opacity: 0, y: 18 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={reduced ? undefined : { opacity: 0, scale: 0.96 }}
|
||||||
|
transition={reduced ? { duration: 0 } : { ...springs.gentle, delay }}
|
||||||
|
whileHover={
|
||||||
|
reduced
|
||||||
|
? undefined
|
||||||
|
: { y: -3, borderColor: "var(--color-ink-700)" }
|
||||||
|
}
|
||||||
|
whileTap={reduced ? undefined : { scale: 0.985 }}
|
||||||
|
aria-label={`${target.name} — ${t.state[target.state]}`}
|
||||||
|
>
|
||||||
|
{/* faint state wash at the top edge */}
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className="absolute inset-x-0 top-0 h-px opacity-70 transition-opacity group-hover:opacity-100"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(to right, transparent, ${STATE_COLOR[target.state]}, transparent)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<motion.div layoutId={`ring-${target.id}`} transition={springs.layout}>
|
||||||
|
<StatusRing target={target} uptime={uptime} delay={delay + 0.05} />
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<h3 className="mt-2 text-[13px] text-paper font-medium leading-snug">
|
||||||
|
{target.name}
|
||||||
|
</h3>
|
||||||
|
<p
|
||||||
|
className="mt-1 text-[10px] text-paper-faint truncate max-w-full tabular font-mono"
|
||||||
|
dir="ltr"
|
||||||
|
>
|
||||||
|
{target.endpoint}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-3 flex items-center gap-2 text-[10px] font-mono">
|
||||||
|
<span
|
||||||
|
className="size-[5px] rounded-full"
|
||||||
|
style={{ background: STATE_COLOR[target.state] }}
|
||||||
|
/>
|
||||||
|
<span style={{ color: STATE_COLOR[target.state] }}>
|
||||||
|
{t.state[target.state]}
|
||||||
|
</span>
|
||||||
|
<span className="text-ink-700">·</span>
|
||||||
|
<span className="text-paper-faint uppercase tracking-wider">
|
||||||
|
{target.type}
|
||||||
|
</span>
|
||||||
|
<span className="text-ink-700">·</span>
|
||||||
|
<span className="text-paper-faint tabular">
|
||||||
|
{t.interval} {fmtInterval(lang, target.interval_sec)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</motion.button>
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user