11/**
22 * GovernanceEventCard - Governance event display
3- *
3+ *
44 * Handles proposal and voting related events with appropriate icons,
55 * colors, and actions based on event type.
66 */
99
1010import Link from "next/link" ;
1111import { formatDistanceToNow } from "date-fns" ;
12- import {
13- Target ,
14- ThumbsUp ,
15- ThumbsDown ,
16- MinusCircle ,
17- Clock ,
12+ import {
13+ AlertCircle ,
1814 CheckCircle ,
19- XCircle ,
15+ Clock ,
16+ MessageSquare ,
17+ MinusCircle ,
2018 ShieldX ,
21- AlertCircle
19+ Target ,
20+ ThumbsDown ,
21+ ThumbsUp ,
22+ XCircle ,
2223} from "lucide-react" ;
23- import { Card , CardContent } from "@/components/ui/card" ;
2424import { AddressDisplay } from "@/components/ui/address-display" ;
25- import { cn } from "@/lib/utils " ;
25+ import { Card , CardContent } from "@/components/ui/card " ;
2626import type { FeedEvent } from "@/lib/types/feed-events" ;
27+ import { cn } from "@/lib/utils" ;
2728
2829export interface GovernanceEventCardProps {
2930 event : Extract < FeedEvent , { category : "governance" } > ;
@@ -33,29 +34,23 @@ export interface GovernanceEventCardProps {
3334
3435export function GovernanceEventCard ( { event, compact, sequenceNumber } : GovernanceEventCardProps ) {
3536 // Removed: timeAgo is redundant since we have day headers
36-
37+
3738 // Event icon and color based on type
3839 const { icon : Icon , iconColor, bgColor, title, actionText } = getEventDisplay ( event ) ;
3940
4041 return (
41- < Card className = { cn (
42- "transition-shadow hover:shadow-md relative" ,
43- compact ? "py-3" : "py-4"
44- ) } >
42+ < Card className = { cn ( "transition-shadow hover:shadow-md relative" , compact ? "py-3" : "py-4" ) } >
4543 < CardContent className = { cn ( compact ? "px-4 py-2" : "px-4" ) } >
4644 { /* Sequence number badge */ }
4745 { sequenceNumber !== undefined && (
4846 < div className = "absolute top-3 right-3 flex items-center justify-center w-7 h-7 rounded-full bg-muted text-muted-foreground text-xs font-semibold" >
4947 { sequenceNumber }
5048 </ div >
5149 ) }
52-
50+
5351 < div className = "flex items-start gap-3" >
5452 { /* Event icon */ }
55- < div className = { cn (
56- "flex-shrink-0 rounded-full p-2" ,
57- bgColor
58- ) } >
53+ < div className = { cn ( "flex-shrink-0 rounded-full p-2" , bgColor ) } >
5954 < Icon className = { cn ( "h-4 w-4" , iconColor ) } />
6055 </ div >
6156
@@ -67,7 +62,7 @@ export function GovernanceEventCard({ event, compact, sequenceNumber }: Governan
6762 { event . type === "VoteCast" ? (
6863 < p className = "text-sm font-medium" >
6964 Vote on{ " " }
70- < Link
65+ < Link
7166 href = { `/proposals/${ event . proposalNumber } ` }
7267 className = "underline hover:opacity-80"
7368 >
@@ -81,26 +76,21 @@ export function GovernanceEventCard({ event, compact, sequenceNumber }: Governan
8176 </ div >
8277
8378 { /* Event-specific content */ }
84- { event . type === "ProposalCreated" && (
85- < ProposalCreatedContent event = { event } />
86- ) }
87- { event . type === "VoteCast" && (
88- < VoteCastContent event = { event } compact = { compact } />
89- ) }
90- { ( event . type === "ProposalQueued" ||
91- event . type === "ProposalExecuted" ||
79+ { event . type === "ProposalCreated" && < ProposalCreatedContent event = { event } /> }
80+ { event . type === "VoteCast" && < VoteCastContent event = { event } compact = { compact } /> }
81+ { ( event . type === "ProposalQueued" ||
82+ event . type === "ProposalExecuted" ||
9283 event . type === "ProposalCanceled" ||
93- event . type === "ProposalVetoed" ) && (
94- < ProposalStatusContent event = { event } />
95- ) }
84+ event . type === "ProposalVetoed" ) && < ProposalStatusContent event = { event } /> }
9685 { ( event . type === "VotingOpened" || event . type === "VotingClosingSoon" ) && (
9786 < VotingAlertContent event = { event } />
9887 ) }
88+ { event . type === "ProposalUpdated" && < ProposalUpdatedContent event = { event } /> }
9989
10090 { /* Action button */ }
10191 { actionText && (
10292 < div className = "pt-1" >
103- < Link
93+ < Link
10494 href = { getEventLink ( event ) }
10595 className = "text-xs text-primary hover:underline font-medium"
10696 >
@@ -117,13 +107,17 @@ export function GovernanceEventCard({ event, compact, sequenceNumber }: Governan
117107
118108// Subcomponents for different event types
119109
120- function ProposalCreatedContent ( { event } : { event : Extract < FeedEvent , { type : "ProposalCreated" } > } ) {
110+ function ProposalCreatedContent ( {
111+ event,
112+ } : {
113+ event : Extract < FeedEvent , { type : "ProposalCreated" } > ;
114+ } ) {
121115 return (
122116 < div className = "space-y-1.5" >
123117 < p className = "text-sm font-semibold line-clamp-2" > { event . title } </ p >
124118 < div className = "flex items-center gap-1 text-xs text-muted-foreground" >
125119 < span > by</ span >
126- < AddressDisplay
120+ < AddressDisplay
127121 address = { event . proposer }
128122 variant = "compact"
129123 showAvatar = { true }
@@ -140,23 +134,26 @@ function ProposalCreatedContent({ event }: { event: Extract<FeedEvent, { type: "
140134 ) ;
141135}
142136
143- function VoteCastContent ( { event, compact } : {
144- event : Extract < FeedEvent , { type : "VoteCast" } > ;
137+ function VoteCastContent ( {
138+ event,
139+ compact,
140+ } : {
141+ event : Extract < FeedEvent , { type : "VoteCast" } > ;
145142 compact ?: boolean ;
146143} ) {
147144 const supportConfig = {
148145 FOR : { icon : ThumbsUp , color : "text-green-600" , bg : "bg-green-50 dark:bg-green-950" } ,
149146 AGAINST : { icon : ThumbsDown , color : "text-red-600" , bg : "bg-red-50 dark:bg-red-950" } ,
150147 ABSTAIN : { icon : MinusCircle , color : "text-gray-600" , bg : "bg-gray-50 dark:bg-gray-950" } ,
151148 } ;
152-
149+
153150 const config = supportConfig [ event . support ] ;
154151 const SupportIcon = config . icon ;
155-
152+
156153 return (
157154 < div className = "space-y-1.5" >
158155 < div className = "flex items-center gap-2 flex-wrap" >
159- < AddressDisplay
156+ < AddressDisplay
160157 address = { event . voter }
161158 variant = "compact"
162159 showAvatar = { true }
@@ -166,13 +163,17 @@ function VoteCastContent({ event, compact }: {
166163 showExplorer = { false }
167164 />
168165 < span className = "text-xs text-muted-foreground" > voted</ span >
169- < span className = { cn ( "flex items-center gap-1 text-xs font-medium" , config . bg , "px-2 py-0.5 rounded-md" ) } >
166+ < span
167+ className = { cn (
168+ "flex items-center gap-1 text-xs font-medium" ,
169+ config . bg ,
170+ "px-2 py-0.5 rounded-md" ,
171+ ) }
172+ >
170173 < SupportIcon className = { cn ( "h-3 w-3" , config . color ) } />
171174 { event . support }
172175 </ span >
173- < span className = "text-xs text-muted-foreground" >
174- with { event . weight } votes
175- </ span >
176+ < span className = "text-xs text-muted-foreground" > with { event . weight } votes</ span >
176177 </ div >
177178 < p className = "text-xs font-medium line-clamp-1" > { event . proposalTitle } </ p >
178179 { event . reason && ! compact && (
@@ -184,8 +185,13 @@ function VoteCastContent({ event, compact }: {
184185 ) ;
185186}
186187
187- function ProposalStatusContent ( { event } : {
188- event : Extract < FeedEvent , { type : "ProposalQueued" | "ProposalExecuted" | "ProposalCanceled" | "ProposalVetoed" } >
188+ function ProposalStatusContent ( {
189+ event,
190+ } : {
191+ event : Extract <
192+ FeedEvent ,
193+ { type : "ProposalQueued" | "ProposalExecuted" | "ProposalCanceled" | "ProposalVetoed" }
194+ > ;
189195} ) {
190196 return (
191197 < div className = "space-y-1" >
@@ -201,19 +207,55 @@ function ProposalStatusContent({ event }: {
201207 ) ;
202208}
203209
204- function VotingAlertContent ( { event } : {
205- event : Extract < FeedEvent , { type : "VotingOpened" | "VotingClosingSoon" } >
210+ function ProposalUpdatedContent ( {
211+ event,
212+ } : {
213+ event : Extract < FeedEvent , { type : "ProposalUpdated" } > ;
214+ } ) {
215+ // Builder propdate enum: 0 = original/update, 1 = reply. Anything else
216+ // falls back to "update" wording so future enum additions don't crash.
217+ const isReply = event . messageType === 1 ;
218+ return (
219+ < div className = "space-y-1.5" >
220+ < div className = "flex items-center gap-1 text-xs text-muted-foreground" >
221+ < span > { isReply ? "reply from" : "update from" } </ span >
222+ < AddressDisplay
223+ address = { event . proposer }
224+ variant = "compact"
225+ showAvatar = { true }
226+ avatarSize = "xs"
227+ showENS = { true }
228+ showCopy = { false }
229+ showExplorer = { false }
230+ />
231+ < span > on</ span >
232+ < Link href = { `/proposals/${ event . proposalNumber } ` } className = "underline hover:opacity-80" >
233+ Proposal #{ event . proposalNumber }
234+ </ Link >
235+ </ div >
236+ { event . message && (
237+ < p className = "text-xs italic text-muted-foreground border-l-2 border-muted pl-2 line-clamp-3" >
238+ { event . message }
239+ </ p >
240+ ) }
241+ </ div >
242+ ) ;
243+ }
244+
245+ function VotingAlertContent ( {
246+ event,
247+ } : {
248+ event : Extract < FeedEvent , { type : "VotingOpened" | "VotingClosingSoon" } > ;
206249} ) {
207250 return (
208251 < div className = "space-y-1" >
209252 < p className = "text-sm font-medium" >
210253 Proposal #{ event . proposalNumber } : { event . proposalTitle }
211254 </ p >
212255 < p className = "text-xs text-muted-foreground" suppressHydrationWarning >
213- { event . type === "VotingOpened"
256+ { event . type === "VotingOpened"
214257 ? `Voting ends ${ formatDistanceToNow ( new Date ( event . voteEnd * 1000 ) , { addSuffix : true } ) } `
215- : `Only ${ event . hoursLeft } h left to vote!`
216- }
258+ : `Only ${ event . hoursLeft } h left to vote!` }
217259 </ p >
218260 </ div >
219261 ) ;
@@ -233,9 +275,24 @@ function getEventDisplay(event: Extract<FeedEvent, { category: "governance" }>)
233275 } ;
234276 case "VoteCast" :
235277 return {
236- icon : event . support === "FOR" ? ThumbsUp : event . support === "AGAINST" ? ThumbsDown : MinusCircle ,
237- iconColor : event . support === "FOR" ? "text-green-600" : event . support === "AGAINST" ? "text-red-600" : "text-gray-600" ,
238- bgColor : event . support === "FOR" ? "bg-green-50 dark:bg-green-950" : event . support === "AGAINST" ? "bg-red-50 dark:bg-red-950" : "bg-gray-50 dark:bg-gray-950" ,
278+ icon :
279+ event . support === "FOR"
280+ ? ThumbsUp
281+ : event . support === "AGAINST"
282+ ? ThumbsDown
283+ : MinusCircle ,
284+ iconColor :
285+ event . support === "FOR"
286+ ? "text-green-600"
287+ : event . support === "AGAINST"
288+ ? "text-red-600"
289+ : "text-gray-600" ,
290+ bgColor :
291+ event . support === "FOR"
292+ ? "bg-green-50 dark:bg-green-950"
293+ : event . support === "AGAINST"
294+ ? "bg-red-50 dark:bg-red-950"
295+ : "bg-gray-50 dark:bg-gray-950" ,
239296 title : `Vote on Proposal #${ event . proposalNumber } ` ,
240297 actionText : "View Proposal" ,
241298 } ;
@@ -287,13 +344,23 @@ function getEventDisplay(event: Extract<FeedEvent, { category: "governance" }>)
287344 title : "Voting Closing Soon" ,
288345 actionText : "Cast Vote" ,
289346 } ;
347+ case "ProposalUpdated" :
348+ return {
349+ icon : MessageSquare ,
350+ iconColor : "text-indigo-600" ,
351+ bgColor : "bg-indigo-50 dark:bg-indigo-950" ,
352+ title : `Propdate · Proposal #${ event . proposalNumber } ` ,
353+ actionText : "View Propdate" ,
354+ } ;
290355 }
291356}
292357
293358function getEventLink ( event : Extract < FeedEvent , { category : "governance" } > ) : string {
359+ if ( event . type === "ProposalUpdated" ) {
360+ return `/propdates#proposal-${ event . proposalNumber } ` ;
361+ }
294362 if ( "proposalNumber" in event ) {
295363 return `/proposals/${ event . proposalNumber } ` ;
296364 }
297365 return "/proposals" ;
298366}
299-
0 commit comments