From 95b82c60aae823ed631c656bace2bdf0ca4265b8 Mon Sep 17 00:00:00 2001 From: Akshay Date: Thu, 6 Nov 2025 09:20:59 +0530 Subject: [PATCH 1/4] feat(connections): Add comprehensive user connections management system - Implement connections page with following, followers, and search tabs - Create ConnectionStats component to display follower and following counts - Add FollowersList and FollowingList components for user connection management - Integrate SearchUsers component for finding and connecting with other users - Update protected layout sidebar to include Connections navigation item - Implement dynamic loading and error handling for connection-related data - Use Supabase and custom connection service for data retrieval - Add responsive design with Tabs and UserCard components - Enhance user experience with intuitive connection management interface --- app/protected/connections/page.tsx | 63 ++++++++ app/protected/layout.tsx | 16 ++- components/connections/ConnectionStats.tsx | 70 +++++++++ components/connections/FollowersList.tsx | 112 +++++++++++++++ components/connections/FollowingList.tsx | 104 ++++++++++++++ components/connections/SearchUsers.tsx | 115 +++++++++++++++ components/connections/UserCard.tsx | 158 +++++++++++++++++++++ components/connections/index.ts | 5 + 8 files changed, 638 insertions(+), 5 deletions(-) create mode 100644 app/protected/connections/page.tsx create mode 100644 components/connections/ConnectionStats.tsx create mode 100644 components/connections/FollowersList.tsx create mode 100644 components/connections/FollowingList.tsx create mode 100644 components/connections/SearchUsers.tsx create mode 100644 components/connections/UserCard.tsx create mode 100644 components/connections/index.ts diff --git a/app/protected/connections/page.tsx b/app/protected/connections/page.tsx new file mode 100644 index 00000000..07c62b81 --- /dev/null +++ b/app/protected/connections/page.tsx @@ -0,0 +1,63 @@ +'use client' + +import React, { useState } from 'react' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { Input } from '@/components/ui/input' +import { Search, Users } from 'lucide-react' +import { FollowingList } from '@/components/connections/FollowingList' +import { FollowersList } from '@/components/connections/FollowersList' +import { SearchUsers } from '@/components/connections/SearchUsers' +import { ConnectionStats } from '@/components/connections/ConnectionStats' + +export default function ConnectionsPage() { + const [searchQuery, setSearchQuery] = useState('') + + return ( +
+ {/* Header */} +
+
+
+ +

Connections

+
+ +
+
+ + {/* Main Content */} +
+
+ + + Following + Followers + Search + + + + + + + + + + + +
+ + setSearchQuery(e.target.value)} + className="pl-9" + /> +
+ +
+
+
+
+
+ ) +} diff --git a/app/protected/layout.tsx b/app/protected/layout.tsx index 7f1e335f..1a8084bf 100644 --- a/app/protected/layout.tsx +++ b/app/protected/layout.tsx @@ -123,6 +123,16 @@ const sidebarItems: SidebarGroupType[] = [ { title: "Community", items: [ + { + title: "Connections", + url: "/protected/connections", + icon: Users, + }, + { + title: "Messages", + url: "/protected/messages", + icon: MessageSquare, + }, { title: "Study Groups", url: "/protected/study-groups", @@ -188,11 +198,7 @@ const sidebarItems: SidebarGroupType[] = [ { title: "Support", items: [ - { - title: "Messages", - url: "/protected/messages", - icon: MessageSquare, - }, + { title: "Help Center", url: "/protected/help", diff --git a/components/connections/ConnectionStats.tsx b/components/connections/ConnectionStats.tsx new file mode 100644 index 00000000..19e34249 --- /dev/null +++ b/components/connections/ConnectionStats.tsx @@ -0,0 +1,70 @@ +'use client' + +import React, { useEffect, useState } from 'react' +import { connectionService } from '@/lib/services/connectionService' +import { useAuth } from '@/lib/hooks/useAuth' +import { Card } from '@/components/ui/card' +import { Users, UserPlus } from 'lucide-react' + +export function ConnectionStats() { + const { user } = useAuth() + const [stats, setStats] = useState({ + following: 0, + followers: 0 + }) + const [loading, setLoading] = useState(true) + + useEffect(() => { + if (!user) return + + const loadStats = async () => { + try { + const [following, followers] = await Promise.all([ + connectionService.getFollowingCount(user.id), + connectionService.getFollowerCount(user.id) + ]) + setStats({ following, followers }) + } catch (error) { + console.error('Error loading connection stats:', error) + } finally { + setLoading(false) + } + } + + loadStats() + }, [user]) + + if (loading) { + return ( +
+ +
+
+ + +
+
+ +
+ ) + } + + return ( +
+ +
+ + Following +
+

{stats.following}

+
+ +
+ + Followers +
+

{stats.followers}

+
+
+ ) +} diff --git a/components/connections/FollowersList.tsx b/components/connections/FollowersList.tsx new file mode 100644 index 00000000..a1db90dd --- /dev/null +++ b/components/connections/FollowersList.tsx @@ -0,0 +1,112 @@ +'use client' + +import React, { useEffect, useState } from 'react' +import { createClient } from '@/lib/supabase/client' +import { useAuth } from '@/lib/hooks/useAuth' +import { UserCard } from './UserCard' +import { Loader2, Users } from 'lucide-react' +import { connectionService } from '@/lib/services/connectionService' + +interface UserProfile { + id: string + first_name: string | null + last_name: string | null + username: string + avatar_url: string | null + bio: string | null +} + +export function FollowersList() { + const { user } = useAuth() + const [followers, setFollowers] = useState([]) + const [connectionStatuses, setConnectionStatuses] = useState>({}) + const [loading, setLoading] = useState(true) + + const loadFollowers = React.useCallback(async () => { + if (!user) return + + try { + setLoading(true) + const supabase = createClient() + + const { data, error } = await supabase + .from('user_connections') + .select(` + follower_id, + profiles:follower_id ( + id, + first_name, + last_name, + username, + avatar_url, + bio + ) + `) + .eq('following_id', user.id) + .order('created_at', { ascending: false }) + + if (error) throw error + + const users = (data || []) + .map(item => item.profiles as unknown) + .filter((profile: unknown): profile is UserProfile => + profile !== null && + typeof profile === 'object' && + 'id' in profile + ) + + setFollowers(users) + + // Load connection statuses for all followers + const statuses: Record = {} + await Promise.all( + users.map(async (profile) => { + const status = await connectionService.getConnectionStatus(profile.id) + statuses[profile.id] = status + }) + ) + setConnectionStatuses(statuses) + } catch (error) { + console.error('Error loading followers:', error) + } finally { + setLoading(false) + } + }, [user]) + + useEffect(() => { + loadFollowers() + }, [loadFollowers]) + + if (loading) { + return ( +
+ +
+ ) + } + + if (followers.length === 0) { + return ( +
+ +

No followers yet

+

+ When people follow you, they'll appear here +

+
+ ) + } + + return ( +
+ {followers.map((profile) => ( + + ))} +
+ ) +} diff --git a/components/connections/FollowingList.tsx b/components/connections/FollowingList.tsx new file mode 100644 index 00000000..95ee3b1d --- /dev/null +++ b/components/connections/FollowingList.tsx @@ -0,0 +1,104 @@ +'use client' + +import React, { useEffect, useState } from 'react' +import { createClient } from '@/lib/supabase/client' +import { useAuth } from '@/lib/hooks/useAuth' +import { UserCard } from './UserCard' +import { Loader2, Users } from 'lucide-react' + +interface UserProfile { + id: string + first_name: string | null + last_name: string | null + username: string + avatar_url: string | null + bio: string | null +} + +export function FollowingList() { + const { user } = useAuth() + const [following, setFollowing] = useState([]) + const [loading, setLoading] = useState(true) + + const loadFollowing = React.useCallback(async () => { + if (!user) return + + try { + setLoading(true) + const supabase = createClient() + + const { data, error } = await supabase + .from('user_connections') + .select(` + following_id, + profiles:following_id ( + id, + first_name, + last_name, + username, + avatar_url, + bio + ) + `) + .eq('follower_id', user.id) + .order('created_at', { ascending: false }) + + if (error) throw error + + const users = (data || []) + .map(item => item.profiles as unknown) + .filter((profile: unknown): profile is UserProfile => + profile !== null && + typeof profile === 'object' && + 'id' in profile + ) + + setFollowing(users) + } catch (error) { + console.error('Error loading following:', error) + } finally { + setLoading(false) + } + }, [user]) + + useEffect(() => { + loadFollowing() + }, [loadFollowing]) + + if (loading) { + return ( +
+ +
+ ) + } + + if (following.length === 0) { + return ( +
+ +

No connections yet

+

+ Start following users to build your network +

+
+ ) + } + + return ( +
+ {following.map((profile) => ( + + ))} +
+ ) +} diff --git a/components/connections/SearchUsers.tsx b/components/connections/SearchUsers.tsx new file mode 100644 index 00000000..07f5be0e --- /dev/null +++ b/components/connections/SearchUsers.tsx @@ -0,0 +1,115 @@ +'use client' + +import React, { useEffect, useState } from 'react' +import { UserCard } from './UserCard' +import { Loader2, Search } from 'lucide-react' +import { conversationService } from '@/lib/services/conversationService' +import { connectionService } from '@/lib/services/connectionService' + +interface UserProfile { + id: string + first_name: string | null + last_name: string | null + username: string + avatar_url: string | null + bio?: string | null +} + +interface SearchUsersProps { + searchQuery: string +} + +export function SearchUsers({ searchQuery }: SearchUsersProps) { + const [users, setUsers] = useState([]) + const [connectionStatuses, setConnectionStatuses] = useState>({}) + const [loading, setLoading] = useState(false) + + useEffect(() => { + const searchUsers = async () => { + if (searchQuery.trim().length < 2) { + setUsers([]) + return + } + + try { + setLoading(true) + const results = await conversationService.searchUsers(searchQuery) + setUsers(results) + + // Load connection statuses for all users + const statuses: Record = {} + await Promise.all( + results.map(async (user) => { + const status = await connectionService.getConnectionStatus(user.id) + statuses[user.id] = status + }) + ) + setConnectionStatuses(statuses) + } catch (error) { + console.error('Error searching users:', error) + } finally { + setLoading(false) + } + } + + const debounce = setTimeout(searchUsers, 300) + return () => clearTimeout(debounce) + }, [searchQuery]) + + const handleConnectionChange = async () => { + // Reload connection statuses + const statuses: Record = {} + await Promise.all( + users.map(async (user) => { + const status = await connectionService.getConnectionStatus(user.id) + statuses[user.id] = status + }) + ) + setConnectionStatuses(statuses) + } + + if (loading) { + return ( +
+ +
+ ) + } + + if (searchQuery.trim().length < 2) { + return ( +
+ +

Search for users

+

+ Type at least 2 characters to search +

+
+ ) + } + + if (users.length === 0) { + return ( +
+ +

No users found

+

+ Try a different search term +

+
+ ) + } + + return ( +
+ {users.map((user) => ( + + ))} +
+ ) +} diff --git a/components/connections/UserCard.tsx b/components/connections/UserCard.tsx new file mode 100644 index 00000000..0336aa25 --- /dev/null +++ b/components/connections/UserCard.tsx @@ -0,0 +1,158 @@ +'use client' + +import React, { useState } from 'react' +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' +import { MessageCircle, UserMinus, UserPlus } from 'lucide-react' +import { connectionService } from '@/lib/services/connectionService' +import { conversationService } from '@/lib/services/conversationService' +import { useRouter } from 'next/navigation' + +interface UserCardProps { + user: { + id: string + first_name: string | null + last_name: string | null + username: string + avatar_url: string | null + bio?: string | null + } + connectionStatus?: { + isFollowing: boolean + isFollower: boolean + isMutual: boolean + } + onConnectionChange?: () => void + showMessageButton?: boolean +} + +export function UserCard({ user, connectionStatus, onConnectionChange, showMessageButton = true }: UserCardProps) { + const router = useRouter() + const [loading, setLoading] = useState(false) + const [localStatus, setLocalStatus] = useState(connectionStatus) + + const name = `${user.first_name || ''} ${user.last_name || ''}`.trim() || user.username + const initials = `${user.first_name?.[0] || ''}${user.last_name?.[0] || ''}`.toUpperCase() || user.username[0].toUpperCase() + + const handleFollow = async () => { + try { + setLoading(true) + await connectionService.followUser(user.id) + setLocalStatus(prev => prev ? { ...prev, isFollowing: true, isMutual: prev.isFollower } : undefined) + onConnectionChange?.() + } catch (error) { + console.error('Error following user:', error) + alert(error instanceof Error ? error.message : 'Failed to follow user') + } finally { + setLoading(false) + } + } + + const handleUnfollow = async () => { + try { + setLoading(true) + await connectionService.unfollowUser(user.id) + setLocalStatus(prev => prev ? { ...prev, isFollowing: false, isMutual: false } : undefined) + onConnectionChange?.() + } catch (error) { + console.error('Error unfollowing user:', error) + alert(error instanceof Error ? error.message : 'Failed to unfollow user') + } finally { + setLoading(false) + } + } + + const handleMessage = async () => { + try { + setLoading(true) + const { canMessage, reason } = await conversationService.canMessageUser(user.id) + + if (!canMessage) { + alert(reason || 'Cannot message this user') + return + } + + const conversation = await conversationService.getOrCreateConversation(user.id) + router.push(`/protected/messages?conversation=${conversation.id}`) + } catch (error) { + console.error('Error creating conversation:', error) + alert(error instanceof Error ? error.message : 'Failed to create conversation') + } finally { + setLoading(false) + } + } + + return ( +
+ + {user.avatar_url && } + + {initials} + + + +
+
+
+

{name}

+
+

@{user.username}

+ {localStatus?.isMutual && ( + + Connected + + )} + {!localStatus?.isMutual && localStatus?.isFollower && ( + + Follows you + + )} +
+ {user.bio && ( +

{user.bio}

+ )} +
+ +
+ {showMessageButton && localStatus?.isMutual && ( + + )} + + {localStatus?.isFollowing ? ( + + ) : ( + + )} +
+
+
+
+ ) +} diff --git a/components/connections/index.ts b/components/connections/index.ts new file mode 100644 index 00000000..b0d99f17 --- /dev/null +++ b/components/connections/index.ts @@ -0,0 +1,5 @@ +export { ConnectionStats } from './ConnectionStats' +export { FollowingList } from './FollowingList' +export { FollowersList } from './FollowersList' +export { SearchUsers } from './SearchUsers' +export { UserCard } from './UserCard' From 1c281a135d23e83d3f05a6b2ce5eed34f242f74c Mon Sep 17 00:00:00 2001 From: Akshay Date: Thu, 6 Nov 2025 09:50:41 +0530 Subject: [PATCH 2/4] feat(connections): Enhance UserCard with interactive profile view and improved UX - Add UserProfileModal component for detailed user profile display - Implement clickable avatar and username to open profile modal - Add 'View Profile' button with Eye icon - Improve hover states and transitions for user interaction - Add hover effects to avatar and username - Enhance button layout and responsiveness - Include view profile functionality with modal trigger --- components/connections/UserCard.tsx | 170 ++++++---- components/connections/UserProfileModal.tsx | 336 ++++++++++++++++++++ 2 files changed, 447 insertions(+), 59 deletions(-) create mode 100644 components/connections/UserProfileModal.tsx diff --git a/components/connections/UserCard.tsx b/components/connections/UserCard.tsx index 0336aa25..44ec893f 100644 --- a/components/connections/UserCard.tsx +++ b/components/connections/UserCard.tsx @@ -4,10 +4,11 @@ import React, { useState } from 'react' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' -import { MessageCircle, UserMinus, UserPlus } from 'lucide-react' +import { MessageCircle, UserMinus, UserPlus, Eye } from 'lucide-react' import { connectionService } from '@/lib/services/connectionService' import { conversationService } from '@/lib/services/conversationService' import { useRouter } from 'next/navigation' +import { UserProfileModal } from './UserProfileModal' interface UserCardProps { user: { @@ -31,6 +32,7 @@ export function UserCard({ user, connectionStatus, onConnectionChange, showMessa const router = useRouter() const [loading, setLoading] = useState(false) const [localStatus, setLocalStatus] = useState(connectionStatus) + const [showProfileModal, setShowProfileModal] = useState(false) const name = `${user.first_name || ''} ${user.last_name || ''}`.trim() || user.username const initials = `${user.first_name?.[0] || ''}${user.last_name?.[0] || ''}`.toUpperCase() || user.username[0].toUpperCase() @@ -83,76 +85,126 @@ export function UserCard({ user, connectionStatus, onConnectionChange, showMessa } } + const handleViewProfile = () => { + setShowProfileModal(true) + } + return ( -
- - {user.avatar_url && } - - {initials} - - + <> +
+ {/* Clickable Avatar */} +
+ + {user.avatar_url && } + + {initials} + + +
-
-
-
-

{name}

-
-

@{user.username}

- {localStatus?.isMutual && ( - - Connected - - )} - {!localStatus?.isMutual && localStatus?.isFollower && ( - - Follows you - +
+
+
+ {/* Clickable Name */} + +
+ + {localStatus?.isMutual && ( + + Connected + + )} + {!localStatus?.isMutual && localStatus?.isFollower && ( + + Follows you + + )} +
+ {user.bio && ( +

{user.bio}

)}
- {user.bio && ( -

{user.bio}

- )} -
-
- {showMessageButton && localStatus?.isMutual && ( - - )} - - {localStatus?.isFollowing ? ( +
+ {/* View Profile Button */} - ) : ( - - )} + + {showMessageButton && localStatus?.isMutual && ( + + )} + + {localStatus?.isFollowing ? ( + + ) : ( + + )} +
-
+ + {/* Profile Preview Modal */} + { + onConnectionChange?.() + // Refresh local status + setLocalStatus(prev => prev ? { ...prev } : undefined) + }} + /> + ) } diff --git a/components/connections/UserProfileModal.tsx b/components/connections/UserProfileModal.tsx new file mode 100644 index 00000000..dbc4b642 --- /dev/null +++ b/components/connections/UserProfileModal.tsx @@ -0,0 +1,336 @@ +'use client' + +import React, { useEffect, useState } from 'react' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' +import { Separator } from '@/components/ui/separator' +import { + MapPin, + Briefcase, + Building, + Github, + Linkedin, + Twitter, + Loader2, + ExternalLink, + MessageCircle, + UserPlus, + UserMinus +} from 'lucide-react' +import { createClient } from '@/lib/supabase/client' +import { connectionService } from '@/lib/services/connectionService' +import { conversationService } from '@/lib/services/conversationService' +import { useRouter } from 'next/navigation' +import Link from 'next/link' + +interface UserProfileModalProps { + userId: string + open: boolean + onOpenChange: (open: boolean) => void + onConnectionChange?: () => void +} + +interface ProfileData { + id: string + first_name: string | null + last_name: string | null + username: string + avatar_url: string | null + bio: string | null + location: string | null + current_position: string | null + company: string | null + skills: string[] | null + github_url: string | null + linkedin_url: string | null + twitter_url: string | null + is_public: boolean +} + +export function UserProfileModal({ + userId, + open, + onOpenChange, + onConnectionChange +}: UserProfileModalProps) { + const router = useRouter() + const [profile, setProfile] = useState(null) + const [loading, setLoading] = useState(true) + const [actionLoading, setActionLoading] = useState(false) + const [connectionStatus, setConnectionStatus] = useState({ + isFollowing: false, + isFollower: false, + isMutual: false + }) + + const loadProfile = React.useCallback(async () => { + try { + setLoading(true) + const supabase = createClient() + const { data, error } = await supabase + .from('profiles') + .select('*') + .eq('id', userId) + .single() + + if (error) throw error + setProfile(data) + } catch (error) { + console.error('Error loading profile:', error) + } finally { + setLoading(false) + } + }, [userId]) + + const loadConnectionStatus = React.useCallback(async () => { + try { + const status = await connectionService.getConnectionStatus(userId) + setConnectionStatus(status) + } catch (error) { + console.error('Error loading connection status:', error) + } + }, [userId]) + + useEffect(() => { + if (open && userId) { + loadProfile() + loadConnectionStatus() + } + }, [open, userId, loadProfile, loadConnectionStatus]) + + const handleFollow = async () => { + try { + setActionLoading(true) + await connectionService.followUser(userId) + await loadConnectionStatus() + onConnectionChange?.() + } catch (error) { + console.error('Error following user:', error) + alert(error instanceof Error ? error.message : 'Failed to follow user') + } finally { + setActionLoading(false) + } + } + + const handleUnfollow = async () => { + try { + setActionLoading(true) + await connectionService.unfollowUser(userId) + await loadConnectionStatus() + onConnectionChange?.() + } catch (error) { + console.error('Error unfollowing user:', error) + alert(error instanceof Error ? error.message : 'Failed to unfollow user') + } finally { + setActionLoading(false) + } + } + + const handleMessage = async () => { + try { + setActionLoading(true) + const { canMessage, reason } = await conversationService.canMessageUser(userId) + + if (!canMessage) { + alert(reason || 'Cannot message this user') + return + } + + const conversation = await conversationService.getOrCreateConversation(userId) + onOpenChange(false) + router.push(`/protected/messages?conversation=${conversation.id}`) + } catch (error) { + console.error('Error creating conversation:', error) + alert(error instanceof Error ? error.message : 'Failed to create conversation') + } finally { + setActionLoading(false) + } + } + + if (loading || !profile) { + return ( + + +
+ +
+
+
+ ) + } + + const name = `${profile.first_name || ''} ${profile.last_name || ''}`.trim() || profile.username + const initials = `${profile.first_name?.[0] || ''}${profile.last_name?.[0] || ''}`.toUpperCase() || profile.username[0].toUpperCase() + + return ( + + + + User Profile + + + {/* Profile Header */} +
+ + {profile.avatar_url && } + + {initials} + + + +
+

{name}

+

@{profile.username}

+
+ + {/* Connection Badges */} +
+ {connectionStatus.isMutual && ( + Connected + )} + {!connectionStatus.isMutual && connectionStatus.isFollower && ( + Follows you + )} +
+ + {/* Action Buttons */} +
+ {connectionStatus.isMutual && ( + + )} + + {connectionStatus.isFollowing ? ( + + ) : ( + + )} + + +
+
+ + + + {/* Profile Details */} +
+ {/* Bio */} + {profile.bio && ( +
+

About

+

{profile.bio}

+
+ )} + + {/* Professional Info */} + {(profile.current_position || profile.company || profile.location) && ( +
+ {profile.current_position && ( +
+ + {profile.current_position} +
+ )} + {profile.company && ( +
+ + {profile.company} +
+ )} + {profile.location && ( +
+ + {profile.location} +
+ )} +
+ )} + + {/* Skills */} + {profile.skills && profile.skills.length > 0 && ( +
+

Skills

+
+ {profile.skills.map((skill, index) => ( + + {skill} + + ))} +
+
+ )} + + {/* Social Links */} + {(profile.github_url || profile.linkedin_url || profile.twitter_url) && ( +
+

Social Links

+
+ {profile.github_url && ( + + )} + {profile.linkedin_url && ( + + )} + {profile.twitter_url && ( + + )} +
+
+ )} +
+
+
+ ) +} From 4f06954e51bad260cba6a3cd7632835ce620ba81 Mon Sep 17 00:00:00 2001 From: Akshay Date: Thu, 6 Nov 2025 10:51:22 +0530 Subject: [PATCH 3/4] feat(connections): Enhance connections page with interactive navigation and improved UX - Add dynamic tab management with state tracking in connections page - Implement interactive connection stats cards with hover and click interactions - Update tab triggers with icons for better visual clarity - Add shimmer loading animation for connection stats loading state - Improve tab navigation with programmatic tab changing - Enhance visual feedback and interactivity for connection statistics cards --- app/globals.css | 14 +++++++ app/protected/connections/page.tsx | 22 ++++++++--- components/connections/ConnectionStats.tsx | 43 +++++++++++++--------- components/connections/UserCard.tsx | 16 ++++++-- 4 files changed, 68 insertions(+), 27 deletions(-) diff --git a/app/globals.css b/app/globals.css index e6040daf..e55202fb 100644 --- a/app/globals.css +++ b/app/globals.css @@ -459,3 +459,17 @@ Accessibility: Enhanced focus indicators for keyboard navigation */ .skip-link:focus { top: 0; } + +/* Shimmer animation for loading states */ +@keyframes shimmer { + 0% { + transform: translateX(-100%); + } + 100% { + transform: translateX(100%); + } +} + +.animate-shimmer { + animation: shimmer 2s infinite; +} diff --git a/app/protected/connections/page.tsx b/app/protected/connections/page.tsx index 07c62b81..7c3b1cdc 100644 --- a/app/protected/connections/page.tsx +++ b/app/protected/connections/page.tsx @@ -3,7 +3,7 @@ import React, { useState } from 'react' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Input } from '@/components/ui/input' -import { Search, Users } from 'lucide-react' +import { Search, Users, UserPlus } from 'lucide-react' import { FollowingList } from '@/components/connections/FollowingList' import { FollowersList } from '@/components/connections/FollowersList' import { SearchUsers } from '@/components/connections/SearchUsers' @@ -11,6 +11,7 @@ import { ConnectionStats } from '@/components/connections/ConnectionStats' export default function ConnectionsPage() { const [searchQuery, setSearchQuery] = useState('') + const [activeTab, setActiveTab] = useState('following') return (
@@ -21,18 +22,27 @@ export default function ConnectionsPage() {

Connections

- +
{/* Main Content */}
- + - Following - Followers - Search + + + Following + + + + Followers + + + + Search + diff --git a/components/connections/ConnectionStats.tsx b/components/connections/ConnectionStats.tsx index 19e34249..a13cd30c 100644 --- a/components/connections/ConnectionStats.tsx +++ b/components/connections/ConnectionStats.tsx @@ -6,7 +6,11 @@ import { useAuth } from '@/lib/hooks/useAuth' import { Card } from '@/components/ui/card' import { Users, UserPlus } from 'lucide-react' -export function ConnectionStats() { +interface ConnectionStatsProps { + onTabChange?: (tab: string) => void +} + +export function ConnectionStats({ onTabChange }: ConnectionStatsProps) { const { user } = useAuth() const [stats, setStats] = useState({ following: 0, @@ -37,33 +41,38 @@ export function ConnectionStats() { if (loading) { return (
- -
-
- - -
-
- + {[1, 2].map((i) => ( + +
+
+
+ + ))}
) } return (
- -
+ onTabChange?.('following')} + > +
- Following + Following
-

{stats.following}

+

{stats.following}

- -
+ onTabChange?.('followers')} + > +
- Followers + Followers
-

{stats.followers}

+

{stats.followers}

) diff --git a/components/connections/UserCard.tsx b/components/connections/UserCard.tsx index 44ec893f..38f6228f 100644 --- a/components/connections/UserCard.tsx +++ b/components/connections/UserCard.tsx @@ -4,7 +4,7 @@ import React, { useState } from 'react' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' -import { MessageCircle, UserMinus, UserPlus, Eye } from 'lucide-react' +import { MessageCircle, UserMinus, UserPlus, Eye, CheckCircle2, UserCheck } from 'lucide-react' import { connectionService } from '@/lib/services/connectionService' import { conversationService } from '@/lib/services/conversationService' import { useRouter } from 'next/navigation' @@ -126,18 +126,26 @@ export function UserCard({ user, connectionStatus, onConnectionChange, showMessa @{user.username} {localStatus?.isMutual && ( - + + Connected )} + {!localStatus?.isMutual && localStatus?.isFollowing && ( + + + Following + + )} {!localStatus?.isMutual && localStatus?.isFollower && ( - + + Follows you )}
{user.bio && ( -

{user.bio}

+

{user.bio}

)}
From 2c4a1494e21e231ef8da7624a5efa5ae534a9d27 Mon Sep 17 00:00:00 2001 From: Akshay Date: Thu, 6 Nov 2025 11:10:16 +0530 Subject: [PATCH 4/4] feat(connections): Enhance user search and empty state UI with improved interactions - Add clear search button to search input for better UX - Redesign empty state views for followers, following, and search tabs - Improve search input placeholder and styling - Add subtle animations and visual indicators for empty states - Enhance search functionality with minimum character requirement feedback - Improve overall visual hierarchy and user guidance in connections page --- app/protected/connections/page.tsx | 28 ++++++++- components/connections/FollowersList.tsx | 23 +++++-- components/connections/FollowingList.tsx | 23 +++++-- components/connections/SearchUsers.tsx | 70 +++++++++++++++------ components/connections/UserProfileModal.tsx | 16 ++++- 5 files changed, 124 insertions(+), 36 deletions(-) diff --git a/app/protected/connections/page.tsx b/app/protected/connections/page.tsx index 7c3b1cdc..873e4b26 100644 --- a/app/protected/connections/page.tsx +++ b/app/protected/connections/page.tsx @@ -55,13 +55,35 @@ export default function ConnectionsPage() {
- + setSearchQuery(e.target.value)} - className="pl-9" + className="pl-9 pr-9" /> + {searchQuery && ( + + )}
diff --git a/components/connections/FollowersList.tsx b/components/connections/FollowersList.tsx index a1db90dd..7cfb379b 100644 --- a/components/connections/FollowersList.tsx +++ b/components/connections/FollowersList.tsx @@ -87,12 +87,23 @@ export function FollowersList() { if (followers.length === 0) { return ( -
- -

No followers yet

-

- When people follow you, they'll appear here -

+
+
+
+
+ +
+
+
+

No followers yet

+

+ When people follow you, they'll appear here. Keep engaging with the community! +

+
+
+
+ Share your profile to gain followers +
) } diff --git a/components/connections/FollowingList.tsx b/components/connections/FollowingList.tsx index 95ee3b1d..05b6fa5e 100644 --- a/components/connections/FollowingList.tsx +++ b/components/connections/FollowingList.tsx @@ -75,12 +75,23 @@ export function FollowingList() { if (following.length === 0) { return ( -
- -

No connections yet

-

- Start following users to build your network -

+
+
+
+
+ +
+
+
+

No connections yet

+

+ Start following users to build your professional network and stay connected +

+
+
+
+ Try the Search tab to find people +
) } diff --git a/components/connections/SearchUsers.tsx b/components/connections/SearchUsers.tsx index 07f5be0e..347dbf14 100644 --- a/components/connections/SearchUsers.tsx +++ b/components/connections/SearchUsers.tsx @@ -78,38 +78,68 @@ export function SearchUsers({ searchQuery }: SearchUsersProps) { if (searchQuery.trim().length < 2) { return ( -
- -

Search for users

-

- Type at least 2 characters to search -

+
+
+
+
+ +
+
+
+

Discover New Connections

+

+ Search for users by name or username to expand your network +

+
+
+
+ Type at least 2 characters to start searching +
) } if (users.length === 0) { return ( -
- -

No users found

-

- Try a different search term +

+
+
+ +
+
+
+

No users found

+

+ We couldn't find anyone matching "{searchQuery}" +

+
+

+ Try searching with a different name or username

) } return ( -
- {users.map((user) => ( - - ))} +
+ {/* Result count header */} +
+

+ Found {users.length} {users.length === 1 ? 'user' : 'users'} +

+
+ + {/* User list */} +
+ {users.map((user) => ( + + ))} +
) } diff --git a/components/connections/UserProfileModal.tsx b/components/connections/UserProfileModal.tsx index dbc4b642..7319f374 100644 --- a/components/connections/UserProfileModal.tsx +++ b/components/connections/UserProfileModal.tsx @@ -105,6 +105,20 @@ export function UserProfileModal({ } }, [open, userId, loadProfile, loadConnectionStatus]) + // Keyboard navigation + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape' && open) { + onOpenChange(false) + } + } + + if (open) { + document.addEventListener('keydown', handleKeyDown) + return () => document.removeEventListener('keydown', handleKeyDown) + } + }, [open, onOpenChange]) + const handleFollow = async () => { try { setActionLoading(true) @@ -171,7 +185,7 @@ export function UserProfileModal({ return ( - + User Profile