Spaces:
Running
Running
| 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> | |
| ); | |
| } | |