| 1 | import { clsx } from "@/lib/cx"; |
| 2 | import { isAllowedSourceHost } from "@/lib/neutrality"; |
| 3 | |
| 4 | |
| 5 | export function SourceLink({ |
| 6 | url, |
| 7 | label = "Source", |
| 8 | className, |
| 9 | }: { |
| 10 | url: string; |
| 11 | label?: string; |
| 12 | className?: string; |
| 13 | }) { |
| 14 | const allowed = isAllowedSourceHost(url); |
| 15 | const host = safeHost(url); |
| 16 | const safeHref = /^https?:\/\ |
| 17 | return ( |
| 18 | <a |
| 19 | href={safeHref} |
| 20 | target="_blank" |
| 21 | rel="noopener noreferrer nofollow" |
| 22 | className={clsx( |
| 23 | "focus-ring label inline-flex min-h-[40px] max-w-full flex-wrap items-center gap-x-1 gap-y-0.5 rounded-[3px] border border-line px-2 py-1 underline underline-offset-4 transition-colors", |
| 24 | allowed |
| 25 | ? "text-ash hover:text-paper hover:border-paper" |
| 26 | : "text-nay", |
| 27 | className |
| 28 | )} |
| 29 | title={allowed ? `Official record · ${host}` : "Source host not allowlisted"} |
| 30 | > |
| 31 | { |
| 32 | } |
| 33 | <span className="inline-flex items-center gap-x-1 whitespace-nowrap"> |
| 34 | <span aria-hidden="true">↗</span> |
| 35 | <span> |
| 36 | {label} |
| 37 | {allowed && host ? <span className="text-ash-2"> ·</span> : null} |
| 38 | </span> |
| 39 | </span> |
| 40 | {allowed && host ? ( |
| 41 | <span className="text-[11px] tracking-normal text-ash-2 [overflow-wrap:anywhere]"> |
| 42 | {host} |
| 43 | </span> |
| 44 | ) : null} |
| 45 | </a> |
| 46 | ); |
| 47 | } |
| 48 | |
| 49 | function safeHost(url: string): string { |
| 50 | try { |
| 51 | return new URL(url).host.replace(/^www\./, ""); |
| 52 | } catch { |
| 53 | return ""; |
| 54 | } |
| 55 | } |