/ components / StaleBanner.tsx
StaleBanner.tsx
 1  import { useState } from 'react';
 2  import { Check, ChevronDown, ChevronRight, RefreshCw, AlertTriangle, HelpCircle } from 'lucide-react';
 3  import type { FreshnessResult } from '../lib/types';
 4  import { timeAgo } from '../lib/utils';
 5  
 6  interface Props {
 7    freshness: FreshnessResult;
 8    onReReview: () => void;
 9  }
10  
11  const MAX_DISPLAYED_COMMITS = 20;
12  
13  export function StaleBanner({ freshness, onReReview }: Props) {
14    const [expanded, setExpanded] = useState(false);
15  
16    if (freshness.status === 'current') {
17      return (
18        <div className="mx-4 mt-2 flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium staleBanner-current">
19          <Check className="h-3.5 w-3.5" />
20          <span>Up to date</span>
21        </div>
22      );
23    }
24  
25    if (freshness.status === 'unknown') {
26      return (
27        <div className="mx-4 mt-2 flex items-center gap-2 rounded-md p-3 text-sm staleBanner-unknown">
28          <HelpCircle className="h-4 w-4 shrink-0" />
29          <span>Could not check freshness: {freshness.reason}</span>
30        </div>
31      );
32    }
33  
34    if (freshness.status === 'force-pushed') {
35      return (
36        <div className="mx-4 mt-2 flex items-center justify-between gap-2 rounded-md p-3 text-sm staleBanner-warn">
37          <div className="flex items-center gap-2">
38            <AlertTriangle className="h-4 w-4 shrink-0" />
39            <span>PR was force-pushed since this review was generated.</span>
40          </div>
41          <button
42            onClick={onReReview}
43            className="flex shrink-0 items-center gap-1.5 rounded-md px-3 py-1 text-xs font-medium transition-colors staleBanner-warn-btn"
44          >
45            <RefreshCw className="h-3 w-3" />
46            Re-review
47          </button>
48        </div>
49      );
50    }
51  
52    // status === 'stale'
53    const { aheadBy, commits } = freshness;
54    const displayed = commits.slice(0, MAX_DISPLAYED_COMMITS);
55    const overflow = aheadBy - displayed.length;
56  
57    return (
58      <div className="mx-4 mt-2 rounded-md text-sm staleBanner-warn">
59        <div className="flex items-center justify-between gap-2 p-3">
60          <button
61            onClick={() => setExpanded((v) => !v)}
62            className="flex items-center gap-2 text-left transition-colors staleBanner-warn-toggle"
63          >
64            <AlertTriangle className="h-4 w-4 shrink-0" />
65            <span>
66              {aheadBy} commit{aheadBy !== 1 ? 's' : ''} behind
67            </span>
68            {expanded ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
69          </button>
70          <button
71            onClick={onReReview}
72            className="flex shrink-0 items-center gap-1.5 rounded-md px-3 py-1 text-xs font-medium transition-colors staleBanner-warn-btn"
73          >
74            <RefreshCw className="h-3 w-3" />
75            Re-review
76          </button>
77        </div>
78  
79        {expanded && (
80          <ul className="px-3 py-2 space-y-1.5 staleBanner-warn-list">
81            {displayed.map((c) => (
82              <li key={c.sha} className="flex items-baseline gap-2 text-xs">
83                <code className="shrink-0 font-mono staleBanner-warn-sha">{c.sha.slice(0, 7)}</code>
84                <span className="truncate">{c.message}</span>
85                <span className="shrink-0 staleBanner-warn-meta">
86                  {c.authorLogin} {c.authorDate ? `ยท ${timeAgo(c.authorDate)}` : ''}
87                </span>
88              </li>
89            ))}
90            {overflow > 0 && <li className="text-xs staleBanner-warn-meta">and {overflow} more...</li>}
91          </ul>
92        )}
93      </div>
94    );
95  }