Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 16 additions & 10 deletions app/protected/connections/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ export default function ConnectionsPage() {
</div>

{/* Main Content */}
<div className="flex-1 overflow-hidden">
<div className="flex-1 min-h-0 overflow-hidden">
<div className="max-w-7xl mx-auto h-full flex flex-col p-4">
<Tabs value={activeTab} onValueChange={handleTabChange} className="flex-1 flex flex-col">
<TabsList className="grid w-full grid-cols-3 mb-4 h-auto">
<Tabs value={activeTab} onValueChange={handleTabChange} className="flex-1 flex flex-col min-h-0 overflow-hidden">
<TabsList className="grid w-full grid-cols-3 mb-4 h-auto flex-shrink-0">
<TabsTrigger value="following" className="gap-1 sm:gap-2 flex-col sm:flex-row py-2 sm:py-1.5">
<UserPlus className="h-4 w-4" />
<span className="text-xs sm:text-sm">Following</span>
Expand All @@ -60,16 +60,20 @@ export default function ConnectionsPage() {
</TabsTrigger>
</TabsList>

<TabsContent value="following" className="flex-1 overflow-y-auto space-y-3 animate-fadeIn">
<FollowingList />
<TabsContent value="following" className="flex-1 overflow-y-auto min-h-0 mt-0 data-[state=active]:flex data-[state=active]:flex-col animate-fadeIn">
<div className="space-y-3">
<FollowingList />
</div>
</TabsContent>

<TabsContent value="followers" className="flex-1 overflow-y-auto space-y-3 animate-fadeIn">
<FollowersList />
<TabsContent value="followers" className="flex-1 overflow-y-auto min-h-0 mt-0 data-[state=active]:flex data-[state=active]:flex-col animate-fadeIn">
<div className="space-y-3">
<FollowersList />
</div>
</TabsContent>

<TabsContent value="search" className="flex-1 overflow-y-auto space-y-3 animate-fadeIn">
<div className="relative mb-4">
<TabsContent value="search" className="flex-1 min-h-0 mt-0 data-[state=active]:flex data-[state=active]:flex-col animate-fadeIn">
<div className="relative mb-4 flex-shrink-0">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" />
<Input
placeholder="Search by name or username..."
Expand Down Expand Up @@ -100,7 +104,9 @@ export default function ConnectionsPage() {
</button>
)}
</div>
<SearchUsers searchQuery={searchQuery} />
<div className="flex-1 overflow-y-auto min-h-0">
<SearchUsers searchQuery={searchQuery} />
</div>
</TabsContent>
</Tabs>
</div>
Expand Down
103 changes: 103 additions & 0 deletions components/connections/MutualConnections.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
'use client'

import React, { useEffect, useState } from 'react'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Users } from 'lucide-react'
import { connectionService } from '@/lib/services/connectionService'

interface MutualConnectionsProps {
userId: string
className?: string
}

interface MutualUser {
id: string
first_name: string | null
last_name: string | null
username: string
avatar_url: string | null
}

export function MutualConnections({ userId, className = '' }: MutualConnectionsProps) {
const [mutualData, setMutualData] = useState<{
count: number
users: MutualUser[]
}>({ count: 0, users: [] })
const [loading, setLoading] = useState(true)

useEffect(() => {
const loadMutualConnections = async () => {
try {
const data = await connectionService.getMutualConnections(userId)
setMutualData(data)
} catch (error) {
console.error('Error loading mutual connections:', error)
} finally {
setLoading(false)
}
}

loadMutualConnections()
}, [userId])

if (loading || mutualData.count === 0) {
return null
}

const displayUsers = mutualData.users.slice(0, 3)
const remainingCount = mutualData.count - displayUsers.length

const getName = (user: MutualUser) => {
if (user.first_name && user.last_name) {
return `${user.first_name} ${user.last_name}`
}
return user.username
}

const getInitials = (user: MutualUser) => {
if (user.first_name && user.last_name) {
return `${user.first_name[0]}${user.last_name[0]}`.toUpperCase()
}
return user.username.slice(0, 2).toUpperCase()
}

return (
<div className={`flex items-center gap-2 text-sm text-muted-foreground ${className}`}>
<Users className="h-4 w-4 flex-shrink-0" />
<div className="flex items-center gap-1">
{/* Avatar stack */}
<div className="flex -space-x-2">
{displayUsers.map((user) => (
<Avatar key={user.id} className="w-6 h-6 border-2 border-background">
{user.avatar_url && <AvatarImage src={user.avatar_url} alt={getName(user)} />}
<AvatarFallback className="bg-gradient-to-br from-blue-500 to-purple-600 text-white text-xs">
{getInitials(user)}
</AvatarFallback>
</Avatar>
))}
</div>

{/* Text */}
<span className="ml-2">
Connected with{' '}
<span className="font-medium text-foreground">
{displayUsers[0] && getName(displayUsers[0])}
</span>
{displayUsers.length > 1 && (
<>
{' '}and{' '}
<span className="font-medium text-foreground">
{remainingCount > 0
? `${displayUsers.length - 1 + remainingCount} others`
: displayUsers.length === 2
? getName(displayUsers[1])
: `${displayUsers.length - 1} others`
}
</span>
</>
)}
</span>
</div>
</div>
)
}
173 changes: 88 additions & 85 deletions components/connections/UserCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { connectionService } from '@/lib/services/connectionService'
import { conversationService } from '@/lib/services/conversationService'
import { useRouter } from 'next/navigation'
import { UserProfileModal } from './UserProfileModal'
import { MutualConnections } from './MutualConnections'

interface UserCardProps {
user: {
Expand Down Expand Up @@ -91,7 +92,7 @@ export function UserCard({ user, connectionStatus, onConnectionChange, showMessa

return (
<>
<div className="flex items-start gap-4 p-4 rounded-lg border bg-card hover:bg-muted/50 transition-colors group">
<div className="flex items-start gap-4 p-4 rounded-lg border bg-card hover:bg-muted/50 transition-colors group relative">
{/* Clickable Avatar */}
<div
onClick={handleViewProfile}
Expand All @@ -106,101 +107,103 @@ export function UserCard({ user, connectionStatus, onConnectionChange, showMessa
</div>

<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
{/* Clickable Name */}
{/* User Info Section */}
<div className="flex-1 min-w-0 mb-3 sm:mb-0">
{/* Clickable Name */}
<button
onClick={handleViewProfile}
className="text-left group/name w-full"
>
<h3 className="font-semibold truncate hover:text-primary transition-colors inline-flex items-center gap-1 max-w-full">
<span className="truncate">{name}</span>
<Eye className="h-3 w-3 opacity-0 group-hover/name:opacity-100 transition-opacity flex-shrink-0" />
</h3>
</button>
<div className="flex items-center gap-2 flex-wrap mt-1">
<button
onClick={handleViewProfile}
className="text-left group/name"
className="text-sm text-muted-foreground hover:text-primary transition-colors truncate"
>
<h3 className="font-semibold truncate hover:text-primary transition-colors inline-flex items-center gap-1">
{name}
<Eye className="h-3 w-3 opacity-0 group-hover/name:opacity-100 transition-opacity" />
</h3>
@{user.username}
</button>
<div className="flex items-center gap-2 flex-wrap mt-1">
<button
onClick={handleViewProfile}
className="text-sm text-muted-foreground hover:text-primary transition-colors"
>
@{user.username}
</button>
{localStatus?.isMutual && (
<Badge className="text-xs bg-green-500/20 text-green-400 border-green-500/30 hover:bg-green-500/30 gap-1">
<CheckCircle2 className="h-3 w-3" />
Connected
</Badge>
)}
{!localStatus?.isMutual && localStatus?.isFollowing && (
<Badge className="text-xs bg-blue-500/20 text-blue-400 border-blue-500/30 hover:bg-blue-500/30 gap-1">
<UserCheck className="h-3 w-3" />
Following
</Badge>
)}
{!localStatus?.isMutual && localStatus?.isFollower && (
<Badge variant="outline" className="text-xs border-purple-500/30 text-purple-400 gap-1">
<UserPlus className="h-3 w-3" />
Follows you
</Badge>
)}
</div>
{user.bio && (
<p className="text-sm text-muted-foreground mt-2 line-clamp-2 leading-relaxed">{user.bio}</p>
{localStatus?.isMutual && (
<Badge className="text-xs bg-green-500/20 text-green-400 border-green-500/30 hover:bg-green-500/30 gap-1 flex-shrink-0">
<CheckCircle2 className="h-3 w-3" />
Connected
</Badge>
)}
{!localStatus?.isMutual && localStatus?.isFollowing && (
<Badge className="text-xs bg-blue-500/20 text-blue-400 border-blue-500/30 hover:bg-blue-500/30 gap-1 flex-shrink-0">
<UserCheck className="h-3 w-3" />
Following
</Badge>
)}
{!localStatus?.isMutual && localStatus?.isFollower && (
<Badge variant="outline" className="text-xs border-purple-500/30 text-purple-400 gap-1 flex-shrink-0">
<UserPlus className="h-3 w-3" />
Follows you
</Badge>
)}
</div>
{user.bio && (
<p className="text-sm text-muted-foreground mt-2 line-clamp-2 leading-relaxed">{user.bio}</p>
)}
{/* Mutual Connections */}
<MutualConnections userId={user.id} className="mt-2" />
</div>

<div className="flex items-center gap-2 flex-shrink-0">
{/* View Profile Button */}
{/* Action Buttons - Stack on mobile, inline on desktop */}
<div className="flex items-center gap-2 flex-wrap sm:flex-nowrap mt-3 sm:mt-0 sm:absolute sm:top-4 sm:right-4">
{/* View Profile Button */}
<Button
onClick={handleViewProfile}
size="sm"
variant="ghost"
className="gap-1 active:animate-buttonPress flex-shrink-0"
aria-label={`View ${name}'s profile`}
>
<Eye className="h-3 w-3" aria-hidden="true" />
<span className="hidden md:inline">View</span>
</Button>

{showMessageButton && localStatus?.isMutual && (
<Button
onClick={handleViewProfile}
onClick={handleMessage}
disabled={loading}
size="sm"
variant="ghost"
className="gap-1 active:animate-buttonPress"
aria-label={`View ${name}'s profile`}
variant="outline"
className="gap-1 active:animate-buttonPress flex-shrink-0"
aria-label={`Send message to ${name}`}
>
<Eye className="h-3 w-3" aria-hidden="true" />
<span className="hidden md:inline">View</span>
<MessageCircle className="h-3 w-3" aria-hidden="true" />
<span className="hidden sm:inline">Message</span>
</Button>

{showMessageButton && localStatus?.isMutual && (
<Button
onClick={handleMessage}
disabled={loading}
size="sm"
variant="outline"
className="gap-1 active:animate-buttonPress"
aria-label={`Send message to ${name}`}
>
<MessageCircle className="h-3 w-3" aria-hidden="true" />
<span className="hidden sm:inline">Message</span>
</Button>
)}

{localStatus?.isFollowing ? (
<Button
onClick={handleUnfollow}
disabled={loading}
size="sm"
variant="outline"
className="gap-1 active:animate-buttonPress"
aria-label={`Unfollow ${name}`}
>
<UserMinus className="h-3 w-3" aria-hidden="true" />
<span className="hidden sm:inline">Unfollow</span>
</Button>
) : (
<Button
onClick={handleFollow}
disabled={loading}
size="sm"
className="gap-1 active:animate-buttonPress"
aria-label={`Follow ${name}`}
>
<UserPlus className="h-3 w-3" aria-hidden="true" />
<span className="hidden sm:inline">Follow</span>
</Button>
)}
</div>
)}

{localStatus?.isFollowing ? (
<Button
onClick={handleUnfollow}
disabled={loading}
size="sm"
variant="outline"
className="gap-1 active:animate-buttonPress flex-shrink-0"
aria-label={`Unfollow ${name}`}
>
<UserMinus className="h-3 w-3" aria-hidden="true" />
<span className="hidden sm:inline">Unfollow</span>
</Button>
) : (
<Button
onClick={handleFollow}
disabled={loading}
size="sm"
className="gap-1 active:animate-buttonPress flex-shrink-0"
aria-label={`Follow ${name}`}
>
<UserPlus className="h-3 w-3" aria-hidden="true" />
<span className="hidden sm:inline">Follow</span>
</Button>
)}
</div>
</div>
</div>
Expand Down
4 changes: 4 additions & 0 deletions components/connections/UserProfileModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { connectionService } from '@/lib/services/connectionService'
import { conversationService } from '@/lib/services/conversationService'
import { useRouter } from 'next/navigation'
import Link from 'next/link'
import { MutualConnections } from './MutualConnections'

interface UserProfileModalProps {
userId: string
Expand Down Expand Up @@ -214,6 +215,9 @@ export function UserProfileModal({
)}
</div>

{/* Mutual Connections */}
<MutualConnections userId={userId} className="justify-center" />

{/* Action Buttons */}
<div className="flex gap-2 w-full max-w-md">
{connectionStatus.isMutual && (
Expand Down
Loading
Loading