spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1"use client";23import type { SourceKind } from "@dci/core";4import { Archive, ExternalLink } from "lucide-react";5import { fmtDate } from "@/lib/format";6import { SOURCE_KIND_LABEL } from "@/lib/labels";7import { Badge } from "./badge";8import { cn } from "./cn";9import { Tooltip } from "./popover";1011/**12 * Provenance / source-kind badge (OPERATOR · GOVERNMENT · UTILITY · REGULATORY · FILING · DATASET · NEWS · COMMUNITY…).13 * Primary kinds (operator, government, utility, filing, cloud provider, registry) get a filled dot; secondary kinds14 * (news, secondary, dataset, community) a hollow one. Hover / focus shows the source, published & retrieved dates and15 * whether an archived copy exists (documentId).16 */17export interface SourceBadgeProps {18 kind: SourceKind;19 name?: string | null;20 url?: string | null;21 publishedAt?: string | null;22 retrievedAt?: string | null;23 /** an archived document id means the page was captured and can be reprocessed */24 documentId?: string | null;25 size?: "xs" | "sm";26 /** show the source name next to the kind */27 withName?: boolean;28 className?: string;29}3031const PRIMARY: ReadonlySet<SourceKind> = new Set<SourceKind>(["operator", "government", "utility", "filing", "cloud_provider", "registry"]);3233/** Short upper-case kind label used in dense tables ("REGULATORY" for government filings when the source says so). */34export const SOURCE_KIND_CAPS: Record<SourceKind, string> = {35 operator: "OPERATOR",36 government: "GOVERNMENT",37 filing: "FILING",38 utility: "UTILITY",39 dataset: "DATASET",40 community: "COMMUNITY",41 secondary: "SECONDARY",42 news: "NEWS",43 cloud_provider: "CLOUD",44 registry: "REGISTRY",45};4647export function SourceBadge({ kind, name, url, publishedAt, retrievedAt, documentId, size = "xs", withName, className }: SourceBadgeProps) {48 const primary = PRIMARY.has(kind);49 const inner = (50 <Badge tone="outline" size={size} className={cn("cursor-help", className)}>51 <span aria-hidden className={cn("inline-block size-[6px] rounded-full", primary ? "bg-ink-2" : "border-[1.5px] border-ink-3")} />52 <span className="tracking-[0.04em]">{SOURCE_KIND_CAPS[kind]}</span>53 {withName && name && <span className="max-w-[140px] truncate font-normal normal-case tracking-normal text-ink-3">{name}</span>}54 </Badge>55 );56 const details = (57 <div className="flex flex-col gap-0.5">58 <div className="font-medium">{name ?? SOURCE_KIND_LABEL[kind]}</div>59 <div className="opacity-80">60 {SOURCE_KIND_LABEL[kind]} · {primary ? "primary source" : "secondary source"}61 </div>62 {publishedAt && <div className="opacity-80">Published {fmtDate(publishedAt)}</div>}63 {retrievedAt && <div className="opacity-80">Retrieved {fmtDate(retrievedAt)}</div>}64 <div className="inline-flex items-center gap-1 opacity-80">65 <Archive className="size-3" aria-hidden /> {documentId ? "Archived copy on file" : "No archived copy recorded"}66 </div>67 {url && (68 <div className="inline-flex items-center gap-1 truncate opacity-80">69 <ExternalLink className="size-3" aria-hidden /> {safeHost(url)}70 </div>71 )}72 </div>73 );74 return <Tooltip content={details}>{inner}</Tooltip>;75}7677function safeHost(url: string): string {78 try {79 return new URL(url).host.replace(/^www\./, "");80 } catch {81 return url;82 }83}84