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
14 changes: 14 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
95 changes: 95 additions & 0 deletions app/protected/connections/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
'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, UserPlus } 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('')
const [activeTab, setActiveTab] = useState('following')

return (
<div className="flex flex-col h-[calc(100vh-4rem)] bg-black">
{/* Header */}
<div className="border-b p-4">
<div className="max-w-7xl mx-auto">
<div className="flex items-center gap-3 mb-4">
<Users className="h-6 w-6 text-primary" />
<h1 className="text-xl md:text-2xl font-bold">Connections</h1>
</div>
<ConnectionStats onTabChange={setActiveTab} />
</div>
</div>

{/* Main Content */}
<div className="flex-1 overflow-hidden">
<div className="max-w-7xl mx-auto h-full flex flex-col p-4">
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col">
<TabsList className="grid w-full grid-cols-3 mb-4">
<TabsTrigger value="following" className="gap-2">
<UserPlus className="h-4 w-4" />
<span>Following</span>
</TabsTrigger>
<TabsTrigger value="followers" className="gap-2">
<Users className="h-4 w-4" />
<span>Followers</span>
</TabsTrigger>
<TabsTrigger value="search" className="gap-2">
<Search className="h-4 w-4" />
<span>Search</span>
</TabsTrigger>
</TabsList>

<TabsContent value="following" className="flex-1 overflow-y-auto space-y-3">
<FollowingList />
</TabsContent>

<TabsContent value="followers" className="flex-1 overflow-y-auto space-y-3">
<FollowersList />
</TabsContent>

<TabsContent value="search" className="flex-1 overflow-y-auto space-y-3">
<div className="relative mb-4">
<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..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 pr-9"
/>
{searchQuery && (
<button
onClick={() => setSearchQuery('')}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
aria-label="Clear search"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
)}
</div>
<SearchUsers searchQuery={searchQuery} />
</TabsContent>
</Tabs>
</div>
</div>
</div>
)
}
16 changes: 11 additions & 5 deletions app/protected/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -188,11 +198,7 @@ const sidebarItems: SidebarGroupType[] = [
{
title: "Support",
items: [
{
title: "Messages",
url: "/protected/messages",
icon: MessageSquare,
},

{
title: "Help Center",
url: "/protected/help",
Expand Down
79 changes: 79 additions & 0 deletions components/connections/ConnectionStats.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
'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'

interface ConnectionStatsProps {
onTabChange?: (tab: string) => void
}

export function ConnectionStats({ onTabChange }: ConnectionStatsProps) {
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 (
<div className="grid grid-cols-2 gap-4">
{[1, 2].map((i) => (
<Card key={i} className="p-4 relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-muted/50 to-transparent animate-shimmer" />
<div className="h-4 bg-muted rounded w-20 mb-2 animate-pulse" />
<div className="h-8 bg-muted rounded w-12 animate-pulse" />
</Card>
))}
</div>
)
}

return (
<div className="grid grid-cols-2 gap-4">
<Card
className="p-4 cursor-pointer transition-all duration-300 hover:scale-[1.02] hover:shadow-lg hover:border-primary/50 group"
onClick={() => onTabChange?.('following')}
>
<div className="flex items-center gap-2 text-muted-foreground mb-1 group-hover:text-primary transition-colors">
<UserPlus className="h-4 w-4" />
<span className="text-sm font-medium">Following</span>
</div>
<p className="text-2xl font-bold group-hover:text-primary transition-colors">{stats.following}</p>
</Card>
<Card
className="p-4 cursor-pointer transition-all duration-300 hover:scale-[1.02] hover:shadow-lg hover:border-primary/50 group"
onClick={() => onTabChange?.('followers')}
>
<div className="flex items-center gap-2 text-muted-foreground mb-1 group-hover:text-primary transition-colors">
<Users className="h-4 w-4" />
<span className="text-sm font-medium">Followers</span>
</div>
<p className="text-2xl font-bold group-hover:text-primary transition-colors">{stats.followers}</p>
</Card>
</div>
)
}
123 changes: 123 additions & 0 deletions components/connections/FollowersList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
'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<UserProfile[]>([])
const [connectionStatuses, setConnectionStatuses] = useState<Record<string, { isFollowing: boolean; isFollower: boolean; isMutual: boolean }>>({})
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<string, { isFollowing: boolean; isFollower: boolean; isMutual: boolean }> = {}
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 (
<div className="flex items-center justify-center p-12">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
)
}

if (followers.length === 0) {
return (
<div className="flex flex-col items-center justify-center p-16 text-center space-y-6">
<div className="relative">
<div className="absolute inset-0 bg-purple-500/20 blur-3xl rounded-full" />
<div className="relative bg-purple-500/10 p-6 rounded-full border border-purple-500/20">
<Users className="h-16 w-16 text-purple-400" />
</div>
</div>
<div className="space-y-2">
<h3 className="text-xl font-bold">No followers yet</h3>
<p className="text-muted-foreground max-w-md">
When people follow you, they&apos;ll appear here. Keep engaging with the community!
</p>
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<div className="w-2 h-2 rounded-full bg-purple-500 animate-pulse" />
<span>Share your profile to gain followers</span>
</div>
</div>
)
}

return (
<div className="space-y-3">
{followers.map((profile) => (
<UserCard
key={profile.id}
user={profile}
connectionStatus={connectionStatuses[profile.id]}
onConnectionChange={loadFollowers}
/>
))}
</div>
)
}
Loading
Loading