/ 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 }