Sifa professional network frontend (Next.js, React, TailwindCSS) sifa.id/

fix(events): move connection badge inline and restore followers sort (#379)

- Move ConnectionBadge from absolute overlay to inline next to speaker
badge inside IdentityCard, fixing overlap with name/pronouns
- Add connectionType prop to IdentityCard
- Restore "Most followed" sort option that was lost during filter cleanup
- Update test mocks for new Phosphor icon imports

authored by

Guido X Jansen and committed by
GitHub
483dc777 feb4f7a4

+36 -14
+1 -6
src/app/(main)/events/[slug]/event-card-grid.tsx
··· 2 2 3 3 import Link from 'next/link'; 4 4 import { IdentityCard } from '@/components/identity-card'; 5 - import { ConnectionBadge } from '@/components/events/connection-badge'; 6 5 import type { ConnectionMap } from '@/hooks/use-attendee-connections'; 7 6 import type { 8 7 ActiveApp, ··· 74 73 aria-label={`View profile of ${profile.displayName ?? profile.handle}`} 75 74 className="relative h-full transition-transform hover:scale-[1.02] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2" 76 75 > 77 - {connectionType && ( 78 - <div className="absolute right-3 top-3 z-10"> 79 - <ConnectionBadge type={connectionType} handle={profile.handle} /> 80 - </div> 81 - )} 82 76 <IdentityCard 83 77 className="h-full" 84 78 did={profile.did} ··· 102 96 claimed={profile.claimed} 103 97 variant="embed" 104 98 badge={badge} 99 + connectionType={connectionType} 105 100 hideFooter 106 101 /> 107 102 </Link>
+8
src/app/(main)/events/[slug]/event-page-client.tsx
··· 100 100 return getName(a).localeCompare(getName(b)); 101 101 }); 102 102 break; 103 + case 'followers': 104 + copy.sort((a, b) => { 105 + const aCount = (a.profile.followersCount ?? 0) + (a.profile.atprotoFollowersCount ?? 0); 106 + const bCount = (b.profile.followersCount ?? 0) + (b.profile.atprotoFollowersCount ?? 0); 107 + if (bCount !== aCount) return bCount - aCount; 108 + return getName(a).localeCompare(getName(b)); 109 + }); 110 + break; 103 111 case 'alphabetical': 104 112 copy.sort((a, b) => getName(a).localeCompare(getName(b))); 105 113 break;
+5
src/app/(main)/events/__tests__/event-card-enrichment.test.tsx
··· 77 77 ), 78 78 Briefcase: (props: Record<string, unknown>) => <span data-testid="icon-briefcase" {...props} />, 79 79 Buildings: (props: Record<string, unknown>) => <span data-testid="icon-buildings" {...props} />, 80 + ArrowsLeftRight: (props: Record<string, unknown>) => ( 81 + <span data-testid="icon-arrows" {...props} /> 82 + ), 83 + UserCheck: (props: Record<string, unknown>) => <span data-testid="icon-user-check" {...props} />, 84 + UserPlus: (props: Record<string, unknown>) => <span data-testid="icon-user-plus" {...props} />, 80 85 })); 81 86 82 87 // Mock sonner
+2 -1
src/components/events/attendee-filters.tsx
··· 8 8 9 9 export type RoleFilter = 'all' | 'speakers' | 'attendees'; 10 10 export type ConnectionFilter = 'all' | 'mutual' | 'following' | 'followedBy' | 'new'; 11 - export type SortOption = 'connections' | 'alphabetical' | 'speakers'; 11 + export type SortOption = 'connections' | 'alphabetical' | 'speakers' | 'followers'; 12 12 13 13 interface AttendeeFiltersProps { 14 14 onSearchChange: (query: string) => void; ··· 215 215 > 216 216 {isLoggedIn && <option value="connections">Connections first</option>} 217 217 <option value="speakers">Speakers first</option> 218 + <option value="followers">Most followed</option> 218 219 <option value="alphabetical">Alphabetical</option> 219 220 </select> 220 221 </label>
+15 -7
src/components/identity-card.tsx
··· 19 19 Buildings, 20 20 } from '@phosphor-icons/react'; 21 21 import { useProfileEdit } from '@/components/profile-edit-provider'; 22 + import { ConnectionBadge } from '@/components/events/connection-badge'; 22 23 import { Badge } from '@/components/ui/badge'; 23 24 import { Button, buttonVariants } from '@/components/ui/button'; 24 25 import { FollowButton } from '@/components/follow-button'; ··· 94 95 isFollowing?: boolean; 95 96 variant?: 'page' | 'embed'; 96 97 badge?: string; 98 + connectionType?: 'mutual' | 'following' | 'followedBy'; 97 99 hideFooter?: boolean; 98 100 className?: string; 99 101 hasDisplayNameOverride?: boolean; ··· 129 131 isFollowing, 130 132 variant = 'page', 131 133 badge, 134 + connectionType, 132 135 hideFooter, 133 136 className, 134 137 hasDisplayNameOverride, ··· 198 201 </a> 199 202 </div> 200 203 201 - {badge && ( 202 - <Badge 203 - variant="secondary" 204 - className="mt-0.5 w-fit px-1.5 py-0 text-[10px] font-medium" 205 - > 206 - {badge} 207 - </Badge> 204 + {(badge || connectionType) && ( 205 + <div className="mt-0.5 flex flex-wrap items-center gap-1"> 206 + {badge && ( 207 + <Badge 208 + variant="secondary" 209 + className="w-fit px-1.5 py-0 text-[10px] font-medium" 210 + > 211 + {badge} 212 + </Badge> 213 + )} 214 + {connectionType && <ConnectionBadge type={connectionType} handle={handle} />} 215 + </div> 208 216 )} 209 217 210 218 {/* Role at company */}
+5
tests/components/identity-card.test.tsx
··· 84 84 ), 85 85 Briefcase: (props: Record<string, unknown>) => <span data-testid="icon-briefcase" {...props} />, 86 86 Buildings: (props: Record<string, unknown>) => <span data-testid="icon-buildings" {...props} />, 87 + ArrowsLeftRight: (props: Record<string, unknown>) => ( 88 + <span data-testid="icon-arrows" {...props} /> 89 + ), 90 + UserCheck: (props: Record<string, unknown>) => <span data-testid="icon-user-check" {...props} />, 91 + UserPlus: (props: Record<string, unknown>) => <span data-testid="icon-user-plus" {...props} />, 87 92 })); 88 93 89 94 // Mock sonner