Skip to content

Commit 40a1161

Browse files
authored
Merge pull request #274 from codeunia-dev/feat/connections
feat(connections): Complete overhaul of user connections management and UI enhancements
2 parents 73cef35 + 2c4a149 commit 40a1161

File tree

10 files changed

+1155
-5
lines changed

10 files changed

+1155
-5
lines changed

app/globals.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,3 +459,17 @@ Accessibility: Enhanced focus indicators for keyboard navigation */
459459
.skip-link:focus {
460460
top: 0;
461461
}
462+
463+
/* Shimmer animation for loading states */
464+
@keyframes shimmer {
465+
0% {
466+
transform: translateX(-100%);
467+
}
468+
100% {
469+
transform: translateX(100%);
470+
}
471+
}
472+
473+
.animate-shimmer {
474+
animation: shimmer 2s infinite;
475+
}

app/protected/connections/page.tsx

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
'use client'
2+
3+
import React, { useState } from 'react'
4+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
5+
import { Input } from '@/components/ui/input'
6+
import { Search, Users, UserPlus } from 'lucide-react'
7+
import { FollowingList } from '@/components/connections/FollowingList'
8+
import { FollowersList } from '@/components/connections/FollowersList'
9+
import { SearchUsers } from '@/components/connections/SearchUsers'
10+
import { ConnectionStats } from '@/components/connections/ConnectionStats'
11+
12+
export default function ConnectionsPage() {
13+
const [searchQuery, setSearchQuery] = useState('')
14+
const [activeTab, setActiveTab] = useState('following')
15+
16+
return (
17+
<div className="flex flex-col h-[calc(100vh-4rem)] bg-black">
18+
{/* Header */}
19+
<div className="border-b p-4">
20+
<div className="max-w-7xl mx-auto">
21+
<div className="flex items-center gap-3 mb-4">
22+
<Users className="h-6 w-6 text-primary" />
23+
<h1 className="text-xl md:text-2xl font-bold">Connections</h1>
24+
</div>
25+
<ConnectionStats onTabChange={setActiveTab} />
26+
</div>
27+
</div>
28+
29+
{/* Main Content */}
30+
<div className="flex-1 overflow-hidden">
31+
<div className="max-w-7xl mx-auto h-full flex flex-col p-4">
32+
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col">
33+
<TabsList className="grid w-full grid-cols-3 mb-4">
34+
<TabsTrigger value="following" className="gap-2">
35+
<UserPlus className="h-4 w-4" />
36+
<span>Following</span>
37+
</TabsTrigger>
38+
<TabsTrigger value="followers" className="gap-2">
39+
<Users className="h-4 w-4" />
40+
<span>Followers</span>
41+
</TabsTrigger>
42+
<TabsTrigger value="search" className="gap-2">
43+
<Search className="h-4 w-4" />
44+
<span>Search</span>
45+
</TabsTrigger>
46+
</TabsList>
47+
48+
<TabsContent value="following" className="flex-1 overflow-y-auto space-y-3">
49+
<FollowingList />
50+
</TabsContent>
51+
52+
<TabsContent value="followers" className="flex-1 overflow-y-auto space-y-3">
53+
<FollowersList />
54+
</TabsContent>
55+
56+
<TabsContent value="search" className="flex-1 overflow-y-auto space-y-3">
57+
<div className="relative mb-4">
58+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" />
59+
<Input
60+
placeholder="Search by name or username..."
61+
value={searchQuery}
62+
onChange={(e) => setSearchQuery(e.target.value)}
63+
className="pl-9 pr-9"
64+
/>
65+
{searchQuery && (
66+
<button
67+
onClick={() => setSearchQuery('')}
68+
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
69+
aria-label="Clear search"
70+
>
71+
<svg
72+
xmlns="http://www.w3.org/2000/svg"
73+
width="16"
74+
height="16"
75+
viewBox="0 0 24 24"
76+
fill="none"
77+
stroke="currentColor"
78+
strokeWidth="2"
79+
strokeLinecap="round"
80+
strokeLinejoin="round"
81+
>
82+
<line x1="18" y1="6" x2="6" y2="18" />
83+
<line x1="6" y1="6" x2="18" y2="18" />
84+
</svg>
85+
</button>
86+
)}
87+
</div>
88+
<SearchUsers searchQuery={searchQuery} />
89+
</TabsContent>
90+
</Tabs>
91+
</div>
92+
</div>
93+
</div>
94+
)
95+
}

app/protected/layout.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,16 @@ const sidebarItems: SidebarGroupType[] = [
123123
{
124124
title: "Community",
125125
items: [
126+
{
127+
title: "Connections",
128+
url: "/protected/connections",
129+
icon: Users,
130+
},
131+
{
132+
title: "Messages",
133+
url: "/protected/messages",
134+
icon: MessageSquare,
135+
},
126136
{
127137
title: "Study Groups",
128138
url: "/protected/study-groups",
@@ -188,11 +198,7 @@ const sidebarItems: SidebarGroupType[] = [
188198
{
189199
title: "Support",
190200
items: [
191-
{
192-
title: "Messages",
193-
url: "/protected/messages",
194-
icon: MessageSquare,
195-
},
201+
196202
{
197203
title: "Help Center",
198204
url: "/protected/help",
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
'use client'
2+
3+
import React, { useEffect, useState } from 'react'
4+
import { connectionService } from '@/lib/services/connectionService'
5+
import { useAuth } from '@/lib/hooks/useAuth'
6+
import { Card } from '@/components/ui/card'
7+
import { Users, UserPlus } from 'lucide-react'
8+
9+
interface ConnectionStatsProps {
10+
onTabChange?: (tab: string) => void
11+
}
12+
13+
export function ConnectionStats({ onTabChange }: ConnectionStatsProps) {
14+
const { user } = useAuth()
15+
const [stats, setStats] = useState({
16+
following: 0,
17+
followers: 0
18+
})
19+
const [loading, setLoading] = useState(true)
20+
21+
useEffect(() => {
22+
if (!user) return
23+
24+
const loadStats = async () => {
25+
try {
26+
const [following, followers] = await Promise.all([
27+
connectionService.getFollowingCount(user.id),
28+
connectionService.getFollowerCount(user.id)
29+
])
30+
setStats({ following, followers })
31+
} catch (error) {
32+
console.error('Error loading connection stats:', error)
33+
} finally {
34+
setLoading(false)
35+
}
36+
}
37+
38+
loadStats()
39+
}, [user])
40+
41+
if (loading) {
42+
return (
43+
<div className="grid grid-cols-2 gap-4">
44+
{[1, 2].map((i) => (
45+
<Card key={i} className="p-4 relative overflow-hidden">
46+
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-muted/50 to-transparent animate-shimmer" />
47+
<div className="h-4 bg-muted rounded w-20 mb-2 animate-pulse" />
48+
<div className="h-8 bg-muted rounded w-12 animate-pulse" />
49+
</Card>
50+
))}
51+
</div>
52+
)
53+
}
54+
55+
return (
56+
<div className="grid grid-cols-2 gap-4">
57+
<Card
58+
className="p-4 cursor-pointer transition-all duration-300 hover:scale-[1.02] hover:shadow-lg hover:border-primary/50 group"
59+
onClick={() => onTabChange?.('following')}
60+
>
61+
<div className="flex items-center gap-2 text-muted-foreground mb-1 group-hover:text-primary transition-colors">
62+
<UserPlus className="h-4 w-4" />
63+
<span className="text-sm font-medium">Following</span>
64+
</div>
65+
<p className="text-2xl font-bold group-hover:text-primary transition-colors">{stats.following}</p>
66+
</Card>
67+
<Card
68+
className="p-4 cursor-pointer transition-all duration-300 hover:scale-[1.02] hover:shadow-lg hover:border-primary/50 group"
69+
onClick={() => onTabChange?.('followers')}
70+
>
71+
<div className="flex items-center gap-2 text-muted-foreground mb-1 group-hover:text-primary transition-colors">
72+
<Users className="h-4 w-4" />
73+
<span className="text-sm font-medium">Followers</span>
74+
</div>
75+
<p className="text-2xl font-bold group-hover:text-primary transition-colors">{stats.followers}</p>
76+
</Card>
77+
</div>
78+
)
79+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
'use client'
2+
3+
import React, { useEffect, useState } from 'react'
4+
import { createClient } from '@/lib/supabase/client'
5+
import { useAuth } from '@/lib/hooks/useAuth'
6+
import { UserCard } from './UserCard'
7+
import { Loader2, Users } from 'lucide-react'
8+
import { connectionService } from '@/lib/services/connectionService'
9+
10+
interface UserProfile {
11+
id: string
12+
first_name: string | null
13+
last_name: string | null
14+
username: string
15+
avatar_url: string | null
16+
bio: string | null
17+
}
18+
19+
export function FollowersList() {
20+
const { user } = useAuth()
21+
const [followers, setFollowers] = useState<UserProfile[]>([])
22+
const [connectionStatuses, setConnectionStatuses] = useState<Record<string, { isFollowing: boolean; isFollower: boolean; isMutual: boolean }>>({})
23+
const [loading, setLoading] = useState(true)
24+
25+
const loadFollowers = React.useCallback(async () => {
26+
if (!user) return
27+
28+
try {
29+
setLoading(true)
30+
const supabase = createClient()
31+
32+
const { data, error } = await supabase
33+
.from('user_connections')
34+
.select(`
35+
follower_id,
36+
profiles:follower_id (
37+
id,
38+
first_name,
39+
last_name,
40+
username,
41+
avatar_url,
42+
bio
43+
)
44+
`)
45+
.eq('following_id', user.id)
46+
.order('created_at', { ascending: false })
47+
48+
if (error) throw error
49+
50+
const users = (data || [])
51+
.map(item => item.profiles as unknown)
52+
.filter((profile: unknown): profile is UserProfile =>
53+
profile !== null &&
54+
typeof profile === 'object' &&
55+
'id' in profile
56+
)
57+
58+
setFollowers(users)
59+
60+
// Load connection statuses for all followers
61+
const statuses: Record<string, { isFollowing: boolean; isFollower: boolean; isMutual: boolean }> = {}
62+
await Promise.all(
63+
users.map(async (profile) => {
64+
const status = await connectionService.getConnectionStatus(profile.id)
65+
statuses[profile.id] = status
66+
})
67+
)
68+
setConnectionStatuses(statuses)
69+
} catch (error) {
70+
console.error('Error loading followers:', error)
71+
} finally {
72+
setLoading(false)
73+
}
74+
}, [user])
75+
76+
useEffect(() => {
77+
loadFollowers()
78+
}, [loadFollowers])
79+
80+
if (loading) {
81+
return (
82+
<div className="flex items-center justify-center p-12">
83+
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
84+
</div>
85+
)
86+
}
87+
88+
if (followers.length === 0) {
89+
return (
90+
<div className="flex flex-col items-center justify-center p-16 text-center space-y-6">
91+
<div className="relative">
92+
<div className="absolute inset-0 bg-purple-500/20 blur-3xl rounded-full" />
93+
<div className="relative bg-purple-500/10 p-6 rounded-full border border-purple-500/20">
94+
<Users className="h-16 w-16 text-purple-400" />
95+
</div>
96+
</div>
97+
<div className="space-y-2">
98+
<h3 className="text-xl font-bold">No followers yet</h3>
99+
<p className="text-muted-foreground max-w-md">
100+
When people follow you, they&apos;ll appear here. Keep engaging with the community!
101+
</p>
102+
</div>
103+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
104+
<div className="w-2 h-2 rounded-full bg-purple-500 animate-pulse" />
105+
<span>Share your profile to gain followers</span>
106+
</div>
107+
</div>
108+
)
109+
}
110+
111+
return (
112+
<div className="space-y-3">
113+
{followers.map((profile) => (
114+
<UserCard
115+
key={profile.id}
116+
user={profile}
117+
connectionStatus={connectionStatuses[profile.id]}
118+
onConnectionChange={loadFollowers}
119+
/>
120+
))}
121+
</div>
122+
)
123+
}

0 commit comments

Comments
 (0)