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