@@ -20,7 +20,7 @@ import { Transaction } from "@prisma/client";
2020
2121import { TooltipProvider , TooltipTrigger } from "@radix-ui/react-tooltip" ;
2222
23- import { Check , Loader , MoreVertical , X , User , Copy , CheckCircle2 , XCircle , MinusCircle , Vote } from "lucide-react" ;
23+ import { Check , Loader , MoreVertical , X , User , Copy , CheckCircle2 , XCircle , MinusCircle , Vote , ChevronDown , ChevronUp } from "lucide-react" ;
2424import { ToastAction } from "@/components/ui/toast" ;
2525import { Tooltip , TooltipContent } from "@/components/ui/tooltip" ;
2626import DiscordIcon from "@/components/common/discordIcon" ;
@@ -43,6 +43,11 @@ import {
4343 DropdownMenuSeparator ,
4444 DropdownMenuTrigger ,
4545} from "@/components/ui/dropdown-menu" ;
46+ import {
47+ Collapsible ,
48+ CollapsibleContent ,
49+ CollapsibleTrigger ,
50+ } from "@/components/ui/collapsible" ;
4651import { get } from "http" ;
4752import { getProvider } from "@/utils/get-provider" ;
4853import { useSiteStore } from "@/lib/zustand/site" ;
@@ -59,6 +64,7 @@ export default function TransactionCard({
5964 const userAddress = useUserStore ( ( state ) => state . userAddress ) ;
6065 const txJson = JSON . parse ( transaction . txJson ) ;
6166 const [ loading , setLoading ] = useState < boolean > ( false ) ;
67+ const [ isSignersOpen , setIsSignersOpen ] = useState < boolean > ( false ) ;
6268 const { toast } = useToast ( ) ;
6369 const ctx = api . useUtils ( ) ;
6470 const network = useSiteStore ( ( state ) => state . network ) ;
@@ -380,26 +386,52 @@ export default function TransactionCard({
380386 }
381387
382388 if ( ! appWallet ) return < > </ > ;
389+
390+ // Calculate signing threshold info
391+ const signersCount = appWallet . signersAddresses . length ;
392+ const requiredSigners = appWallet . numRequiredSigners ?? signersCount ;
393+ const signedCount = transaction . signedAddresses . length ;
394+ const rejectedCount = transaction . rejectedAddresses . length ;
395+
396+ const getSignersText = ( ) => {
397+ if ( appWallet . type === 'all' ) {
398+ return `All ${ signersCount } signers required` ;
399+ } else if ( appWallet . type === 'any' ) {
400+ return `Any of ${ signersCount } signers` ;
401+ } else {
402+ return `${ requiredSigners } of ${ signersCount } signers` ;
403+ }
404+ } ;
405+
406+ const getRequiredCount = ( ) => {
407+ if ( appWallet . type === 'all' ) {
408+ return signersCount ;
409+ } else if ( appWallet . type === 'any' ) {
410+ return 1 ;
411+ } else {
412+ return requiredSigners ;
413+ }
414+ } ;
415+
416+ const requiredCount = getRequiredCount ( ) ;
417+ const isComplete = signedCount >= requiredCount ;
418+ const progressPercentage = Math . min ( ( signedCount / signersCount ) * 100 , 100 ) ;
419+ const thresholdPercentage = ( requiredCount / signersCount ) * 100 ;
420+ const pendingCount = signersCount - signedCount - rejectedCount ;
421+
383422 return (
384- < Card className = "self-start overflow-hidden" >
385- < CardHeader className = "flex flex-row items-start bg-muted/50 p-4 sm:p-6" >
386- < div className = "grid gap-0.5 flex-1 min-w-0 pr-2" >
387- < CardTitle className = "group flex items-center gap-2 text-base sm:text-lg break-words" >
388- { transaction . description }
389- { /* <Button
390- size="icon"
391- variant="outline"
392- className="h-6 w-6 opacity-0 transition-opacity group-hover:opacity-100"
393- >
394- <Copy className="h-3 w-3" />
395- <span className="sr-only">Copy Order ID</span>
396- </Button> */ }
397- </ CardTitle >
398- < CardDescription className = "text-xs sm:text-sm" >
399- { dateToFormatted ( transaction . createdAt ) }
400- </ CardDescription >
401- </ div >
402- < div className = "ml-auto flex items-center gap-1 flex-shrink-0" >
423+ < Card className = "self-start overflow-hidden w-full" >
424+ < CardHeader className = "flex flex-col gap-3 bg-muted/50 p-4 sm:p-6" >
425+ < div className = "flex flex-row items-start w-full" >
426+ < div className = "grid gap-0.5 flex-1 min-w-0 pr-2" >
427+ < CardTitle className = "group flex items-center gap-2 text-base sm:text-lg break-words" >
428+ { transaction . description }
429+ </ CardTitle >
430+ < CardDescription className = "text-xs sm:text-sm" >
431+ { dateToFormatted ( transaction . createdAt ) }
432+ </ CardDescription >
433+ </ div >
434+ < div className = "ml-auto flex items-center gap-1 flex-shrink-0" >
403435 { /* <Button size="sm" variant="outline" className="h-8 gap-1">
404436 <Truck className="h-3.5 w-3.5" />
405437 <span className="lg:sr-only xl:not-sr-only xl:whitespace-nowrap">
@@ -456,9 +488,73 @@ export default function TransactionCard({
456488 </ DropdownMenuContent >
457489 </ DropdownMenu >
458490 </ div >
491+ </ div >
492+
493+ { /* Signing Threshold - Person Icons with Progress Bar */ }
494+ < div className = "w-full pt-3 border-t border-border/30" >
495+ < div className = "space-y-3" >
496+ < div className = "text-xs font-medium text-muted-foreground" >
497+ { getSignersText ( ) }
498+ </ div >
499+ < div className = "flex items-center gap-1.5" >
500+ { Array . from ( { length : signersCount } ) . map ( ( _ , index ) => {
501+ let iconColor = "text-muted-foreground opacity-30" ; // Light gray (not required, not signed)
502+
503+ if ( index < signedCount ) {
504+ iconColor = "text-green-500 dark:text-green-400" ; // Green (signed, starting from left)
505+ } else if ( index < requiredCount ) {
506+ iconColor = "text-foreground opacity-100" ; // White (threshold requirement, not signed)
507+ }
508+
509+ return (
510+ < User
511+ key = { index }
512+ className = { `h-5 w-5 sm:h-6 sm:w-6 ${ iconColor } ` }
513+ />
514+ ) ;
515+ } ) }
516+ </ div >
517+ { /* Progress Bar */ }
518+ < TooltipProvider >
519+ < Tooltip >
520+ < TooltipTrigger asChild >
521+ < div className = "h-2.5 bg-muted rounded-full overflow-visible shadow-inner relative cursor-help" >
522+ < div
523+ className = "absolute top-0 bottom-0 w-0.5 bg-foreground/40 z-10"
524+ style = { { left : `${ thresholdPercentage } %` } }
525+ >
526+ < div className = "absolute -top-0.5 left-1/2 -translate-x-1/2 w-1 h-1 rounded-full bg-foreground/60" />
527+ </ div >
528+ < div
529+ className = { `h-full transition-all duration-500 ease-out relative rounded-full ${
530+ isComplete
531+ ? "bg-gradient-to-r from-green-500 to-green-600 shadow-sm shadow-green-500/50"
532+ : "bg-gradient-to-r from-primary to-primary/90"
533+ } `}
534+ style = { { width : `${ progressPercentage } %` } }
535+ />
536+ </ div >
537+ </ TooltipTrigger >
538+ < TooltipContent >
539+ < div className = "space-y-1 text-xs" >
540+ < div className = "font-semibold" > { getSignersText ( ) } </ div >
541+ < div className = "text-muted-foreground" >
542+ { signedCount } signed
543+ { rejectedCount > 0 && ` • ${ rejectedCount } rejected` }
544+ { pendingCount > 0 && ` • ${ pendingCount } pending` }
545+ </ div >
546+ < div className = "text-muted-foreground" >
547+ Progress: { signedCount } / { requiredCount } required
548+ </ div >
549+ </ div >
550+ </ TooltipContent >
551+ </ Tooltip >
552+ </ TooltipProvider >
553+ </ div >
554+ </ div >
459555 </ CardHeader >
460556 < CardContent className = "p-4 sm:p-6 text-sm" >
461- < div className = "grid gap-3 sm:gap-4" >
557+ < div className = "grid gap-3 sm:gap-4 max-w-4xl mx-auto w-full " >
462558 { txJson . outputs . length > 0 && (
463559 < >
464560 < div className = "space-y-3" >
@@ -557,118 +653,27 @@ export default function TransactionCard({
557653 </ >
558654 ) }
559655
560- { /* Signer Threshold Visualization */ }
561- { ( ( ) => {
562- const signersCount = appWallet . signersAddresses . length ;
563- const requiredSigners = appWallet . numRequiredSigners ?? signersCount ;
564- const signedCount = transaction . signedAddresses . length ;
565- const rejectedCount = transaction . rejectedAddresses . length ;
566-
567- const getSignersText = ( ) => {
568- if ( appWallet . type === 'all' ) {
569- return `All ${ signersCount } signers required` ;
570- } else if ( appWallet . type === 'any' ) {
571- return `Any of ${ signersCount } signers` ;
572- } else {
573- return `${ requiredSigners } of ${ signersCount } signers` ;
574- }
575- } ;
576-
577- const getRequiredCount = ( ) => {
578- if ( appWallet . type === 'all' ) {
579- return signersCount ;
580- } else if ( appWallet . type === 'any' ) {
581- return 1 ;
582- } else {
583- return requiredSigners ;
584- }
585- } ;
586-
587- const requiredCount = getRequiredCount ( ) ;
588- const isComplete = signedCount >= requiredCount ;
589- const progressPercentage = Math . min ( ( signedCount / signersCount ) * 100 , 100 ) ;
590- const thresholdPercentage = ( requiredCount / signersCount ) * 100 ;
591- const pendingCount = signersCount - signedCount - rejectedCount ;
592-
593- return (
594- < div className = "space-y-4" >
595- { /* Signing Threshold Section */ }
596- < div className = "rounded-lg border border-border/50 bg-muted/30 p-4 space-y-3" >
597- < div className = "space-y-2" >
598- < div >
599- < div className = "text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-1.5" >
600- Signing Threshold
601- </ div >
602- < div className = "text-base font-semibold" > { getSignersText ( ) } </ div >
603- </ div >
604-
605- { /* Progress Bar */ }
606- < div className = "space-y-2" >
607- < div className = "flex items-center justify-between text-xs" >
608- < span className = "text-muted-foreground font-medium" > Progress</ span >
609- < span className = "font-semibold" >
610- { requiredCount } out of { signersCount }
611- </ span >
612- </ div >
613- < div className = "h-2.5 bg-muted rounded-full overflow-visible shadow-inner relative" >
614- { /* Threshold indicator line */ }
615- < div
616- className = "absolute top-0 bottom-0 w-0.5 bg-foreground/40 z-10"
617- style = { { left : `${ thresholdPercentage } %` } }
618- >
619- < div className = "absolute -top-1 left-1/2 -translate-x-1/2 w-1 h-1 rounded-full bg-foreground/60" />
620- </ div >
621- { /* Progress fill */ }
622- < div
623- className = { `h-full transition-all duration-500 ease-out relative rounded-full ${
624- isComplete
625- ? "bg-gradient-to-r from-green-500 to-green-600 shadow-sm shadow-green-500/50"
626- : "bg-gradient-to-r from-primary to-primary/90"
627- } `}
628- style = { { width : `${ progressPercentage } %` } }
629- >
630- { progressPercentage > 0 && (
631- < div className = "absolute inset-0 bg-white/20 animate-pulse rounded-full" />
632- ) }
633- </ div >
634- </ div >
635- </ div >
636-
637- { /* Status Summary */ }
638- < div className = "flex flex-wrap items-center gap-3 pt-1" >
639- < div className = "flex items-center gap-2" >
640- < div className = "h-2.5 w-2.5 rounded-full bg-green-500 shadow-sm shadow-green-500/50" />
641- < span className = "text-xs text-muted-foreground font-medium" >
642- { signedCount } signed
643- </ span >
644- </ div >
645- { rejectedCount > 0 && (
646- < div className = "flex items-center gap-2" >
647- < div className = "h-2.5 w-2.5 rounded-full bg-red-500 shadow-sm shadow-red-500/50" />
648- < span className = "text-xs text-muted-foreground font-medium" >
649- { rejectedCount } rejected
650- </ span >
651- </ div >
652- ) }
653- { pendingCount > 0 && (
654- < div className = "flex items-center gap-2" >
655- < div className = "h-2.5 w-2.5 rounded-full bg-muted-foreground/50 border border-muted-foreground/30" />
656- < span className = "text-xs text-muted-foreground font-medium" >
657- { pendingCount } pending
658- </ span >
659- </ div >
660- ) }
661- </ div >
662- </ div >
663-
656+ { /* Signers List - Collapsible */ }
657+ < Collapsible open = { isSignersOpen } onOpenChange = { setIsSignersOpen } >
658+ < CollapsibleTrigger className = "flex items-center justify-between w-full p-3 rounded-lg border border-border/50 bg-muted/30 hover:bg-muted/50 transition-colors group" >
659+ < div className = "text-xs font-semibold text-muted-foreground uppercase tracking-wide" >
660+ Signers ({ signersCount } )
661+ </ div >
662+ { isSignersOpen ? (
663+ < ChevronUp className = "h-4 w-4 text-muted-foreground transition-transform" />
664+ ) : (
665+ < ChevronDown className = "h-4 w-4 text-muted-foreground transition-transform" />
666+ ) }
667+ </ CollapsibleTrigger >
668+ < CollapsibleContent className = "mt-3 space-y-3" >
664669 { /* Remind All Button */ }
665670 { pendingCount > 0 && (
666- < div className = "pt-3 border-t border-border/50 " >
671+ < div className = "pb-2 " >
667672 < Button
668673 size = "sm"
669674 variant = "outline"
670675 onClick = { handleRemindAll }
671- className = "w-full sm:w-auto hover:bg-primary/10 hover:border-primary/50 transition-colors"
676+ className = "w-full hover:bg-primary/10 hover:border-primary/50 transition-colors"
672677 >
673678 < div className = "flex flex-row items-center gap-1.5" >
674679 < DiscordIcon className = "h-3.5 w-3.5" />
@@ -677,13 +682,6 @@ export default function TransactionCard({
677682 </ Button >
678683 </ div >
679684 ) }
680- </ div >
681-
682- { /* Signers List */ }
683- < div className = "space-y-3" >
684- < div className = "text-xs font-semibold text-muted-foreground uppercase tracking-wide px-1" >
685- Signers
686- </ div >
687685 { appWallet . signersAddresses . map ( ( signerAddress , index ) => {
688686 const hasSigned = transaction . signedAddresses . includes ( signerAddress ) ;
689687 const hasRejected = transaction . rejectedAddresses . includes ( signerAddress ) ;
@@ -782,10 +780,8 @@ export default function TransactionCard({
782780 </ div >
783781 ) ;
784782 } ) }
785- </ div >
786- </ div >
787- ) ;
788- } ) ( ) }
783+ </ CollapsibleContent >
784+ </ Collapsible >
789785 </ div >
790786 </ CardContent >
791787
0 commit comments