SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
4 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
2.9 KB · 61 lines tsx
Raw Blame History
1import type { ProjectSummary } from "@dci/core";2import Link from "next/link";3import { EstimateMark, StatusBadge } from "@/components/ui/badge";4import { CountryFlag } from "@/components/ui/country-flag";5import { EmptyState } from "@/components/ui/states";6import { fmtMw, fmtPartialDateSafe, fmtUsd, placeLine } from "@/lib/format-project";7import { routes } from "@/lib/routes";89/** ProjectSummary rows: name · operator · location · MW · status · expected. */10export function ProjectList({ items, emptyTitle = "No projects yet", emptyDescription = "Announced and under-construction projects will appear once ingested." }: { items: ProjectSummary[]; emptyTitle?: string; emptyDescription?: string }) {11  if (!items.length) return <EmptyState title={emptyTitle} description={emptyDescription} compact />;12  return (13    <ul className="divide-y divide-hair">14      {items.map((p) => (15        <li key={p.id} className="relative grid grid-cols-[minmax(0,1fr)_auto] gap-x-3 py-2.5">16          <div className="min-w-0">17            <div className="flex flex-wrap items-center gap-x-2 gap-y-1">18              {/* stretched link: the whole row opens the project; the operator link sits above via z-index */}19              <Link href={routes.project(p.slug)} className="truncate text-[13.5px] font-medium text-ink hover:text-accent hover:underline after:absolute after:inset-0 after:content-['']">20                {p.name}21              </Link>22              <StatusBadge status={p.status} short size="xs" />23            </div>24            <div className="mt-0.5 flex flex-wrap items-center gap-x-1.5 text-[12px] text-ink-3">25              {p.operator ? (26                <Link href={routes.operator(p.operator.slug)} className="relative z-[1] inline-flex min-h-6 items-center text-ink-2 hover:text-ink">27                  {p.operator.name}28                </Link>29              ) : (30                <span>Operator unknown</span>31              )}32              <span aria-hidden>·</span>33              <CountryFlag iso2={p.countryIso2} size={12} />34              <span className="truncate">{placeLine(p)}</span>35              {p.investmentUsd !== null && (36                <>37                  <span aria-hidden>·</span>38                  <span className="figure">{fmtUsd(p.investmentUsd)}</span>39                </>40              )}41            </div>42          </div>43          <div className="flex flex-col items-end text-right">44            <span className="figure text-[14px] font-medium text-ink">45              {p.plannedMw === null ? <span className="text-ink-4">—</span> : (46                <>47                  <EstimateMark className="mr-0.5 text-[11px]" />48                  {fmtMw(p.plannedMw)}49                </>50              )}51            </span>52            <span className="figure text-[11px] text-ink-3" title="Expected opening">53              {p.expectedOpening ? fmtPartialDateSafe(p.expectedOpening) : "TBD"}54            </span>55          </div>56        </li>57      ))}58    </ul>59  );60}61