Skip to content

Commit cf4d6ee

Browse files
committed
feat(connections): Add mutual connections display for user profiles
- Implement MutualConnections component to show shared connections - Add new service method in connectionService to fetch mutual connections - Integrate mutual connections display in UserCard and UserProfileModal - Enhance user connection information with dynamic avatar stack and text description - Implement fallback and styling for mutual connections display
1 parent 3af491f commit cf4d6ee

4 files changed

Lines changed: 165 additions & 0 deletions

File tree

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
'use client'
2+
3+
import React, { useEffect, useState } from 'react'
4+
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
5+
import { Users } from 'lucide-react'
6+
import { connectionService } from '@/lib/services/connectionService'
7+
8+
interface MutualConnectionsProps {
9+
userId: string
10+
className?: string
11+
}
12+
13+
interface MutualUser {
14+
id: string
15+
first_name: string | null
16+
last_name: string | null
17+
username: string
18+
avatar_url: string | null
19+
}
20+
21+
export function MutualConnections({ userId, className = '' }: MutualConnectionsProps) {
22+
const [mutualData, setMutualData] = useState<{
23+
count: number
24+
users: MutualUser[]
25+
}>({ count: 0, users: [] })
26+
const [loading, setLoading] = useState(true)
27+
28+
useEffect(() => {
29+
const loadMutualConnections = async () => {
30+
try {
31+
const data = await connectionService.getMutualConnections(userId)
32+
setMutualData(data)
33+
} catch (error) {
34+
console.error('Error loading mutual connections:', error)
35+
} finally {
36+
setLoading(false)
37+
}
38+
}
39+
40+
loadMutualConnections()
41+
}, [userId])
42+
43+
if (loading || mutualData.count === 0) {
44+
return null
45+
}
46+
47+
const displayUsers = mutualData.users.slice(0, 3)
48+
const remainingCount = mutualData.count - displayUsers.length
49+
50+
const getName = (user: MutualUser) => {
51+
if (user.first_name && user.last_name) {
52+
return `${user.first_name} ${user.last_name}`
53+
}
54+
return user.username
55+
}
56+
57+
const getInitials = (user: MutualUser) => {
58+
if (user.first_name && user.last_name) {
59+
return `${user.first_name[0]}${user.last_name[0]}`.toUpperCase()
60+
}
61+
return user.username.slice(0, 2).toUpperCase()
62+
}
63+
64+
return (
65+
<div className={`flex items-center gap-2 text-sm text-muted-foreground ${className}`}>
66+
<Users className="h-4 w-4 flex-shrink-0" />
67+
<div className="flex items-center gap-1">
68+
{/* Avatar stack */}
69+
<div className="flex -space-x-2">
70+
{displayUsers.map((user) => (
71+
<Avatar key={user.id} className="w-6 h-6 border-2 border-background">
72+
{user.avatar_url && <AvatarImage src={user.avatar_url} alt={getName(user)} />}
73+
<AvatarFallback className="bg-gradient-to-br from-blue-500 to-purple-600 text-white text-xs">
74+
{getInitials(user)}
75+
</AvatarFallback>
76+
</Avatar>
77+
))}
78+
</div>
79+
80+
{/* Text */}
81+
<span className="ml-2">
82+
Connected with{' '}
83+
<span className="font-medium text-foreground">
84+
{displayUsers[0] && getName(displayUsers[0])}
85+
</span>
86+
{displayUsers.length > 1 && (
87+
<>
88+
{' '}and{' '}
89+
<span className="font-medium text-foreground">
90+
{remainingCount > 0
91+
? `${displayUsers.length - 1 + remainingCount} others`
92+
: displayUsers.length === 2
93+
? getName(displayUsers[1])
94+
: `${displayUsers.length - 1} others`
95+
}
96+
</span>
97+
</>
98+
)}
99+
</span>
100+
</div>
101+
</div>
102+
)
103+
}

components/connections/UserCard.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { connectionService } from '@/lib/services/connectionService'
99
import { conversationService } from '@/lib/services/conversationService'
1010
import { useRouter } from 'next/navigation'
1111
import { UserProfileModal } from './UserProfileModal'
12+
import { MutualConnections } from './MutualConnections'
1213

1314
interface UserCardProps {
1415
user: {
@@ -147,6 +148,8 @@ export function UserCard({ user, connectionStatus, onConnectionChange, showMessa
147148
{user.bio && (
148149
<p className="text-sm text-muted-foreground mt-2 line-clamp-2 leading-relaxed">{user.bio}</p>
149150
)}
151+
{/* Mutual Connections */}
152+
<MutualConnections userId={user.id} className="mt-2" />
150153
</div>
151154

152155
<div className="flex items-center gap-2 flex-shrink-0">

components/connections/UserProfileModal.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { connectionService } from '@/lib/services/connectionService'
2929
import { conversationService } from '@/lib/services/conversationService'
3030
import { useRouter } from 'next/navigation'
3131
import Link from 'next/link'
32+
import { MutualConnections } from './MutualConnections'
3233

3334
interface UserProfileModalProps {
3435
userId: string
@@ -214,6 +215,9 @@ export function UserProfileModal({
214215
)}
215216
</div>
216217

218+
{/* Mutual Connections */}
219+
<MutualConnections userId={userId} className="justify-center" />
220+
217221
{/* Action Buttons */}
218222
<div className="flex gap-2 w-full max-w-md">
219223
{connectionStatus.isMutual && (

lib/services/connectionService.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,61 @@ export class ConnectionService {
179179

180180
return { isFollowing, isFollower, isMutual }
181181
}
182+
183+
// Get mutual connections between current user and another user
184+
async getMutualConnections(userId: string): Promise<{
185+
count: number
186+
users: Array<{
187+
id: string
188+
first_name: string | null
189+
last_name: string | null
190+
username: string
191+
avatar_url: string | null
192+
}>
193+
}> {
194+
const supabase = this.getSupabaseClient()
195+
const { data: { user } } = await supabase.auth.getUser()
196+
197+
if (!user) {
198+
return { count: 0, users: [] }
199+
}
200+
201+
// Get users that both current user and target user follow
202+
const { data: currentUserFollowing } = await supabase
203+
.from('user_connections')
204+
.select('following_id')
205+
.eq('follower_id', user.id)
206+
207+
const { data: targetUserFollowing } = await supabase
208+
.from('user_connections')
209+
.select('following_id')
210+
.eq('follower_id', userId)
211+
212+
if (!currentUserFollowing || !targetUserFollowing) {
213+
return { count: 0, users: [] }
214+
}
215+
216+
// Find mutual following
217+
const currentFollowingIds = new Set(currentUserFollowing.map(c => c.following_id))
218+
const mutualIds = targetUserFollowing
219+
.map(c => c.following_id)
220+
.filter(id => currentFollowingIds.has(id) && id !== user.id && id !== userId)
221+
222+
if (mutualIds.length === 0) {
223+
return { count: 0, users: [] }
224+
}
225+
226+
// Get profile info for first 3 mutual connections
227+
const { data: profiles } = await supabase
228+
.from('profiles')
229+
.select('id, first_name, last_name, username, avatar_url')
230+
.in('id', mutualIds.slice(0, 3))
231+
232+
return {
233+
count: mutualIds.length,
234+
users: profiles || []
235+
}
236+
}
182237
}
183238

184239
export const connectionService = new ConnectionService()

0 commit comments

Comments
 (0)