Spaces:
Running
Running
File size: 5,226 Bytes
2eb87e3 d22337b 2eb87e3 d22337b 2eb87e3 d22337b 2eb87e3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | import { useState } from "react";
import { useNavigate } from "react-router-dom";
import type { ProjectCard } from "@/shared/types";
import { formatRelativeTime, formatStars, totalFindings } from "../shared/lib/format";
import { SeverityBar } from "./SeverityBar";
function OwnerAvatar({ owner, compact = false }: { owner: string; compact?: boolean }) {
const [failed, setFailed] = useState(false);
const sizeClass = compact ? "h-7 w-7" : "h-8 w-8";
if (failed) {
return (
<div
className={`flex shrink-0 items-center justify-center rounded-full bg-surface-sunken font-semibold text-ink-secondary ${sizeClass} ${compact ? "text-[10px]" : "text-xs"}`}
aria-hidden
>
{owner.slice(0, 1).toUpperCase()}
</div>
);
}
return (
<img
src={`https://github.com/${owner}.png?size=64`}
alt=""
width={32}
height={32}
className={`${sizeClass} shrink-0 rounded-full border border-line bg-surface-sunken`}
onError={() => setFailed(true)}
/>
);
}
export function ProjectRow({ project, compact = false }: { project: ProjectCard; compact?: boolean }) {
const nav = useNavigate();
const state = project.latest_scan?.state;
const scanning = state === "scanning" || state === "dispatching";
const waiting = state !== "completed" && !scanning;
const soFar = project.latest_scan?.findings_so_far ?? 0;
const findings = totalFindings(project.severity_counts);
return (
<div
role="link"
tabIndex={0}
onClick={() => nav(`/p/${project.owner_login}/${project.name}`)}
onKeyDown={(e) => {
if (e.key === "Enter") nav(`/p/${project.owner_login}/${project.name}`);
}}
className={`group block cursor-pointer overflow-hidden border-b border-line px-1 transition-colors hover:bg-surface-sunken/60 focus-ring rounded-sm ${
compact ? "h-[104px] py-3" : "py-4"
}`}
>
<div className={`flex items-start ${compact ? "gap-2.5" : "gap-3"}`}>
<OwnerAvatar owner={project.owner_login} compact={compact} />
<div className="min-w-0 flex-1">
<div className="flex items-start justify-between gap-4">
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
<span className={`truncate font-medium text-ink ${compact ? "text-[12px]" : "text-sm"}`}>
{project.full_name}
</span>
</div>
{project.description && (
<p className={`mt-0.5 line-clamp-1 text-ink-secondary ${compact ? "text-[11px] leading-4" : "text-[13px]"}`}>
{project.description}
</p>
)}
<div className={`flex flex-wrap items-center gap-x-2 gap-y-1 text-ink-tertiary ${compact ? "mt-1 text-[10px]" : "mt-1.5 text-[13px]"}`}>
{project.language && (
<span className="inline-flex items-center gap-1">
<span className="h-1.5 w-1.5 rounded-full bg-accent-600/70" />
{project.language}
</span>
)}
<span>·</span>
<span>★ {formatStars(project.stars)}</span>
{project.latest_scan?.finished_at && (
<>
<span>·</span>
<span>scanned {formatRelativeTime(project.latest_scan.finished_at)}</span>
</>
)}
{!waiting && findings > 0 && (
<>
<span>·</span>
<span className="font-mono">{findings} findings</span>
</>
)}
</div>
<div className={compact ? "mt-1.5" : "mt-2.5"}>
{scanning ? (
<span
className={`inline-flex items-center gap-1.5 font-medium text-running-ink ${compact ? "text-[10px]" : "text-[13px]"}`}
>
<span className="relative flex h-1.5 w-1.5">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-running opacity-60 motion-reduce:animate-none" />
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-running" />
</span>
{soFar > 0 ? `Scanning · ${soFar} findings so far` : "Scanning…"}
</span>
) : waiting ? (
<span className={`${compact ? "text-[10px]" : "text-[13px]"} text-ink-tertiary`}>In scan queue</span>
) : compact && findings === 0 ? (
<span className="text-[10px] text-ink-tertiary">No findings</span>
) : (
<SeverityBar
counts={project.severity_counts}
showLegend
widthClass={compact ? "w-32" : "w-40"}
heightClass={compact ? "h-1" : "h-1.5"}
legendClass={compact ? "text-[9px]" : "text-[11px]"}
/>
)}
</div>
</div>
</div>
</div>
</div>
</div>
);
}
|