Fork Horse repository, listed by its owner. Publisher snapshot.

packages/forkhorse-dashboard/src/needs-you.tsx

Snapshot at 34e3175

Containing directory

3,212 bytes · textView raw
1import { useState } from "react";
2import { Badge, Button, LayerCard } from "@cloudflare/kumo";
3import { ArrowLineUp, CaretRight, Check, GitBranch, Warning, XCircle } from "@phosphor-icons/react";
4import type { Brand, ProjectState } from "./contracts.ts";
5import { copyText, ownerNeeds, type CopyPart, type OwnerNeed } from "./needs-you-model.ts";
6
7const icons = {
8  promotionPending: Warning,
9  reconciler: GitBranch,
10  blocker: Warning,
11  failed: XCircle,
12  promote: ArrowLineUp,
13  approve: Check,
14};
15function Copy({ parts }: { parts: CopyPart[] }) {
16  return parts.map((part, index) =>
17    typeof part === "string" ? (
18      part
19    ) : (
20      <code key={index} title={part.full}>
21        {part.code}
22      </code>
23    ),
24  );
25}
26export function NeedsYou({
27  state,
28  brand,
29  open,
30}: {
31  state: ProjectState;
32  brand: Brand;
33  open(target: OwnerNeed["target"]): void;
34}) {
35  const [expanded, setExpanded] = useState(false);
36  const items = ownerNeeds(state, brand);
37  return (
38    <section aria-labelledby="needs-you-title">
39      <LayerCard className="p-4 needs-you-card">
40        <div className="needs-you-heading">
41          <h2 id="needs-you-title" className="heading-3">
42            Needs you
43          </h2>
44          {items.length > 0 && <Badge variant="secondary">{items.length}</Badge>}
45        </div>
46        {items.length ? (
47          <>
48            <ul className="needs-you-list">
49              {(expanded ? items : items.slice(0, 5)).map((item) => {
50                const Icon = icons[item.kind];
51                return (
52                  <li key={item.id} data-need={item.kind}>
53                    <button
54                      type="button"
55                      className="needs-you-row"
56                      aria-label={`${copyText(item.title)}. ${copyText(item.outcome)}`}
57                      onClick={() => open(item.target)}
58                    >
59                      <Icon size={16} aria-hidden="true" className="needs-you-icon" />
60                      <span className="needs-you-copy">
61                        <strong className="needs-you-title">
62                          <Copy parts={item.title} />
63                        </strong>
64                        <span className="caption muted">
65                          <Copy parts={item.why} />
66                        </span>
67                        <span className="caption">
68                          <Copy parts={item.outcome} />
69                        </span>
70                      </span>
71                      <span className="needs-you-open" aria-hidden="true">
72                        Open <CaretRight size={16} aria-hidden="true" />
73                      </span>
74                    </button>
75                  </li>
76                );
77              })}
78            </ul>
79            {!expanded && items.length > 5 && (
80              <Button variant="ghost" size="sm" onClick={() => setExpanded(true)}>
81                Show all {items.length}
82              </Button>
83            )}
84          </>
85        ) : (
86          <p className="needs-you-empty muted">
87            <Check size={16} aria-hidden="true" />
88            Nothing needs you right now.
89          </p>
90        )}
91      </LayerCard>
92    </section>
93  );
94}
95