initial commit: rocket.new export of broadcastbeat

This commit is contained in:
Ryan Salazar
2026-05-07 16:39:17 +00:00
commit 70aa1ad46e
302 changed files with 60710 additions and 0 deletions

View File

@@ -0,0 +1,809 @@
'use client';
import React, { useState, useEffect, useCallback } from 'react';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import { createClient } from '@/lib/supabase/client';
interface UserProfile {
id: string;
full_name: string;
avatar_url: string | null;
bio: string | null;
website: string | null;
location: string | null;
reputation: number;
created_at: string;
}
interface FollowUser {
id: string;
full_name: string;
avatar_url: string | null;
reputation: number;
followed_at: string;
}
interface FollowData {
followerCount: number;
followingCount: number;
isFollowing: boolean;
followers: FollowUser[];
following: FollowUser[];
}
interface ForumThread {
id: string;
title: string;
body: string;
vote_score: number;
upvotes: number;
reply_count: number;
view_count: number;
created_at: string;
forum_categories: { id: string; name: string; slug: string } | null;
}
interface ForumReply {
id: string;
body: string;
vote_score: number;
upvotes: number;
created_at: string;
thread_id: string;
forum_threads: { id: string; title: string } | null;
}
interface CategoryContribution {
name: string;
slug: string;
count: number;
}
interface ProfileStats {
totalThreads: number;
totalReplies: number;
totalPosts: number;
totalUpvotesReceived: number;
}
interface UserBadge {
badge_type: 'contributor' | 'mentor' | 'expert' | 'moderator';
awarded_at: string;
awarded_reason: string | null;
}
type ActiveTab = 'overview' | 'threads' | 'replies' | 'followers' | 'following';
function timeAgo(dateStr: string): string {
const diff = Date.now() - new Date(dateStr).getTime();
const mins = Math.floor(diff / 60000);
if (mins < 2) return 'just now';
if (mins < 60) return `${mins}m ago`;
const hrs = Math.floor(mins / 60);
if (hrs < 24) return `${hrs}h ago`;
const days = Math.floor(hrs / 24);
if (days < 30) return `${days}d ago`;
return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
}
function Avatar({ name, size = 'lg' }: { name: string; size?: 'sm' | 'lg' }) {
const initials = name
.split(' ')
.map((n) => n[0])
.join('')
.toUpperCase()
.slice(0, 2);
const colors = ['#1e3a5f', '#1a3a2a', '#3a1a1a', '#2a1a3a', '#1a2a3a'];
const colorIdx = name.charCodeAt(0) % colors.length;
const sizeClass = size === 'lg' ? 'w-20 h-20 text-2xl' : 'w-9 h-9 text-xs';
return (
<div
className={`${sizeClass} rounded-full flex items-center justify-center flex-shrink-0 text-white font-bold`}
style={{ backgroundColor: colors[colorIdx] }}
>
{initials || '?'}
</div>
);
}
function ReputationBadge({ rep }: { rep: number }) {
let label = 'Newcomer';
let color = 'text-[#888] border-[#333]';
if (rep >= 1000) { label = 'Expert'; color = 'text-yellow-400 border-yellow-700'; }
else if (rep >= 500) { label = 'Veteran'; color = 'text-purple-400 border-purple-700'; }
else if (rep >= 200) { label = 'Contributor'; color = 'text-[#3b82f6] border-[#3b82f6]/50'; }
else if (rep >= 50) { label = 'Member'; color = 'text-green-400 border-green-700'; }
return (
<span className={`font-body text-xs font-bold uppercase tracking-wider px-2 py-0.5 border rounded-sm ${color}`}>
{label}
</span>
);
}
const BADGE_CONFIG: Record<string, { label: string; icon: React.ReactNode; color: string; bg: string; border: string; description: string }> = {
contributor: {
label: 'Contributor',
icon: (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" aria-hidden="true">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
</svg>
),
color: 'text-[#3b82f6]',
bg: 'bg-[#1e3a5f]/40',
border: 'border-[#3b82f6]/40',
description: 'Active community contributor',
},
mentor: {
label: 'Mentor',
icon: (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" aria-hidden="true">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
</svg>
),
color: 'text-green-400',
bg: 'bg-green-900/30',
border: 'border-green-700/40',
description: 'Helps and guides others',
},
expert: {
label: 'Expert',
icon: (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" aria-hidden="true">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2z" />
</svg>
),
color: 'text-yellow-400',
bg: 'bg-yellow-900/30',
border: 'border-yellow-700/40',
description: 'Highly knowledgeable community expert',
},
moderator: {
label: 'Moderator',
icon: (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" aria-hidden="true">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
</svg>
),
color: 'text-purple-400',
bg: 'bg-purple-900/30',
border: 'border-purple-700/40',
description: 'Trusted forum moderator',
},
};
function BadgesSection({ badges }: { badges: UserBadge[] }) {
if (badges.length === 0) return null;
return (
<div className="flex flex-wrap gap-2 mt-3">
{badges.map((badge) => {
const cfg = BADGE_CONFIG[badge.badge_type];
if (!cfg) return null;
return (
<div
key={badge.badge_type}
title={`${cfg.label}: ${cfg.description}${badge.awarded_reason ? `${badge.awarded_reason}` : ''}`}
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-sm border text-xs font-body font-bold uppercase tracking-wider ${cfg.color} ${cfg.bg} ${cfg.border}`}
>
{cfg.icon}
{cfg.label}
</div>
);
})}
</div>
);
}
function BadgesCard({ badges }: { badges: UserBadge[] }) {
if (badges.length === 0) return null;
return (
<div className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-5">
<h2 className="font-heading text-sm font-bold text-[#e0e0e0] uppercase tracking-wider mb-4 pb-3 border-b border-[#2a2a2a]">
Earned Badges
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{badges.map((badge) => {
const cfg = BADGE_CONFIG[badge.badge_type];
if (!cfg) return null;
return (
<div
key={badge.badge_type}
className={`flex items-start gap-3 p-3 rounded-sm border ${cfg.bg} ${cfg.border}`}
>
<div className={`mt-0.5 flex-shrink-0 ${cfg.color}`}>{cfg.icon}</div>
<div className="min-w-0">
<div className={`font-body text-xs font-bold uppercase tracking-wider ${cfg.color}`}>
{cfg.label}
</div>
<div className="font-body text-xs text-[#888] mt-0.5 leading-relaxed">
{cfg.description}
</div>
<div className="font-body text-xs text-[#555] mt-1">
Awarded {new Date(badge.awarded_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
</div>
</div>
</div>
);
})}
</div>
</div>
);
}
export default function UserProfilePage() {
const params = useParams();
const userId = params?.userId as string;
const supabase = createClient();
const [profile, setProfile] = useState<UserProfile | null>(null);
const [threads, setThreads] = useState<ForumThread[]>([]);
const [replies, setReplies] = useState<ForumReply[]>([]);
const [topThreads, setTopThreads] = useState<ForumThread[]>([]);
const [contributionsByCategory, setContributionsByCategory] = useState<CategoryContribution[]>([]);
const [stats, setStats] = useState<ProfileStats | null>(null);
const [badges, setBadges] = useState<UserBadge[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState<ActiveTab>('overview');
const [currentUserId, setCurrentUserId] = useState<string | null>(null);
// Follow state
const [followData, setFollowData] = useState<FollowData>({
followerCount: 0,
followingCount: 0,
isFollowing: false,
followers: [],
following: [],
});
const [followLoading, setFollowLoading] = useState(false);
useEffect(() => {
supabase.auth.getUser().then(({ data: { user } }) => {
setCurrentUserId(user?.id || null);
});
}, []);
const fetchFollowData = useCallback(async (viewerId: string | null) => {
if (!userId) return;
const params = new URLSearchParams({ user_id: userId });
if (viewerId) params.set('viewer_id', viewerId);
const res = await fetch(`/api/forum/follows?${params.toString()}`);
if (res.ok) {
const data = await res.json();
setFollowData(data);
}
}, [userId]);
useEffect(() => {
supabase.auth.getUser().then(({ data: { user } }) => {
const uid = user?.id || null;
setCurrentUserId(uid);
fetchFollowData(uid);
});
}, [fetchFollowData]);
useEffect(() => {
if (!userId) return;
const fetchProfile = async () => {
setLoading(true);
setError(null);
try {
const res = await fetch(`/api/forum/user-profile?user_id=${userId}`);
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Failed to load profile');
setProfile(data.profile);
setThreads(data.threads);
setReplies(data.replies);
setTopThreads(data.topThreads);
setContributionsByCategory(data.contributionsByCategory);
setStats(data.stats);
setBadges(data.badges || []);
} catch (err: any) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchProfile();
}, [userId]);
const handleFollow = async () => {
if (!currentUserId) return;
setFollowLoading(true);
try {
if (followData.isFollowing) {
await fetch(`/api/forum/follows?following_id=${userId}`, { method: 'DELETE' });
setFollowData(prev => ({
...prev,
isFollowing: false,
followerCount: Math.max(0, prev.followerCount - 1),
followers: prev.followers.filter(f => f.id !== currentUserId),
}));
} else {
await fetch('/api/forum/follows', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ following_id: userId }),
});
setFollowData(prev => ({
...prev,
isFollowing: true,
followerCount: prev.followerCount + 1,
}));
// Refresh to get updated follower list
await fetchFollowData(currentUserId);
}
} finally {
setFollowLoading(false);
}
};
const maxCategoryCount = contributionsByCategory[0]?.count || 1;
if (loading) {
return (
<div className="min-h-screen bg-[#111111]">
<Header />
<div className="max-w-container mx-auto px-4 py-12">
<div className="max-w-4xl mx-auto space-y-4">
<div className="skeleton h-32 rounded-sm" />
<div className="skeleton h-64 rounded-sm" />
</div>
</div>
<Footer />
</div>
);
}
if (error || !profile) {
return (
<div className="min-h-screen bg-[#111111]">
<Header />
<div className="max-w-container mx-auto px-4 py-16 text-center">
<p className="font-body text-[#888] text-sm">{error || 'User not found.'}</p>
<Link href="/forum" className="mt-4 inline-block font-body text-xs font-bold uppercase tracking-wider text-[#3b82f6] hover:underline">
Back to Forum
</Link>
</div>
<Footer />
</div>
);
}
const tabs: { id: ActiveTab; label: string; count?: number }[] = [
{ id: 'overview', label: 'Overview' },
{ id: 'threads', label: 'Threads', count: stats?.totalThreads },
{ id: 'replies', label: 'Replies', count: stats?.totalReplies },
{ id: 'followers', label: 'Followers', count: followData.followerCount },
{ id: 'following', label: 'Following', count: followData.followingCount },
];
return (
<div className="min-h-screen bg-[#111111]">
<Header />
{/* Breadcrumb */}
<div className="bg-[#0d0d0d] border-b border-[#1e1e1e]">
<div className="max-w-container mx-auto px-4 py-2.5">
<nav aria-label="Breadcrumb" className="flex items-center gap-2 text-xs font-body text-[#555]">
<Link href="/home-page" className="hover:text-[#3b82f6] transition-colors">Home</Link>
<span aria-hidden="true">/</span>
<Link href="/forum" className="hover:text-[#3b82f6] transition-colors">Forum</Link>
<span aria-hidden="true">/</span>
<span className="text-[#888]">{profile.full_name || 'User Profile'}</span>
</nav>
</div>
</div>
<main className="max-w-container mx-auto px-4 py-8">
<div className="max-w-4xl mx-auto">
{/* Profile Header Card */}
<div className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-6 mb-6">
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-5">
<Avatar name={profile.full_name || 'User'} size="lg" />
<div className="flex-1 min-w-0">
<div className="flex flex-wrap items-center gap-3 mb-1">
<h1 className="font-heading text-2xl font-bold text-[#e8e8e8]">
{profile.full_name || 'Anonymous User'}
</h1>
<ReputationBadge rep={profile.reputation || 0} />
{currentUserId && currentUserId !== userId && (
<button
onClick={handleFollow}
disabled={followLoading}
className={`font-body text-xs font-bold uppercase tracking-wider px-4 py-1.5 rounded-sm border transition-colors disabled:opacity-60 ${
followData.isFollowing
? 'bg-[#1e3a5f] text-[#3b82f6] border-[#3b82f6]/50 hover:bg-red-900/30 hover:text-red-400 hover:border-red-700/50'
: 'bg-[#3b82f6] text-white border-[#3b82f6] hover:bg-[#2563eb]'
}`}
>
{followLoading ? '...' : followData.isFollowing ? 'Following' : '+ Follow'}
</button>
)}
{currentUserId === userId && (
<Link
href="/account"
className="font-body text-xs font-bold uppercase tracking-wider text-[#888] hover:text-[#3b82f6] border border-[#2a2a2a] hover:border-[#3b82f6] px-3 py-1 rounded-sm transition-colors"
>
Edit Profile
</Link>
)}
</div>
{profile.bio && (
<p className="font-body text-sm text-[#aaa] mb-2 leading-relaxed">{profile.bio}</p>
)}
<div className="flex flex-wrap items-center gap-4 text-xs font-body text-[#666]">
{profile.location && (
<span className="flex items-center gap-1">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" />
<circle cx="12" cy="10" r="3" />
</svg>
{profile.location}
</span>
)}
{profile.website && (
<a
href={profile.website.startsWith('http') ? profile.website : `https://${profile.website}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 hover:text-[#3b82f6] transition-colors"
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<circle cx="12" cy="12" r="10" />
<line x1="2" y1="12" x2="22" y2="12" />
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
</svg>
{profile.website.replace(/^https?:\/\//, '')}
</a>
)}
<span className="flex items-center gap-1">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
<line x1="16" y1="2" x2="16" y2="6" />
<line x1="8" y1="2" x2="8" y2="6" />
<line x1="3" y1="10" x2="21" y2="10" />
</svg>
Joined {new Date(profile.created_at).toLocaleDateString('en-US', { month: 'long', year: 'numeric' })}
</span>
</div>
{/* Earned Badges inline */}
<BadgesSection badges={badges} />
</div>
</div>
{/* Reputation & Stats Row */}
<div className="grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-6 gap-3 mt-6 pt-5 border-t border-[#2a2a2a]">
<div className="text-center">
<div className="font-heading text-2xl font-bold text-[#3b82f6]">{(profile.reputation || 0).toLocaleString()}</div>
<div className="font-body text-xs text-[#666] uppercase tracking-wider mt-0.5">Reputation</div>
</div>
<div className="text-center">
<div className="font-heading text-2xl font-bold text-[#e0e0e0]">{stats?.totalThreads || 0}</div>
<div className="font-body text-xs text-[#666] uppercase tracking-wider mt-0.5">Threads</div>
</div>
<div className="text-center">
<div className="font-heading text-2xl font-bold text-[#e0e0e0]">{stats?.totalReplies || 0}</div>
<div className="font-body text-xs text-[#666] uppercase tracking-wider mt-0.5">Replies</div>
</div>
<div className="text-center">
<div className="font-heading text-2xl font-bold text-[#e0e0e0]">{stats?.totalUpvotesReceived || 0}</div>
<div className="font-body text-xs text-[#666] uppercase tracking-wider mt-0.5">Upvotes</div>
</div>
<div className="text-center cursor-pointer" onClick={() => setActiveTab('followers')}>
<div className="font-heading text-2xl font-bold text-[#e0e0e0] hover:text-[#3b82f6] transition-colors">{followData.followerCount}</div>
<div className="font-body text-xs text-[#666] uppercase tracking-wider mt-0.5">Followers</div>
</div>
<div className="text-center cursor-pointer" onClick={() => setActiveTab('following')}>
<div className="font-heading text-2xl font-bold text-[#e0e0e0] hover:text-[#3b82f6] transition-colors">{followData.followingCount}</div>
<div className="font-body text-xs text-[#666] uppercase tracking-wider mt-0.5">Following</div>
</div>
</div>
</div>
{/* Tabs */}
<div className="flex border-b border-[#2a2a2a] mb-6 overflow-x-auto">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`font-body text-sm font-bold uppercase tracking-wider px-5 py-3 border-b-2 transition-colors flex items-center gap-2 whitespace-nowrap ${
activeTab === tab.id
? 'border-[#3b82f6] text-[#3b82f6]'
: 'border-transparent text-[#666] hover:text-[#aaa]'
}`}
>
{tab.label}
{tab.count !== undefined && (
<span className="font-body text-xs bg-[#2a2a2a] text-[#888] px-1.5 py-0.5 rounded-sm">
{tab.count}
</span>
)}
</button>
))}
</div>
{/* Overview Tab */}
{activeTab === 'overview' && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Earned Badges Card */}
{badges.length > 0 && (
<div className="lg:col-span-2">
<BadgesCard badges={badges} />
</div>
)}
{/* Top Threads */}
<div className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-5">
<h2 className="font-heading text-sm font-bold text-[#e0e0e0] uppercase tracking-wider mb-4 pb-3 border-b border-[#2a2a2a]">
Top Threads
</h2>
{topThreads.length === 0 ? (
<p className="font-body text-sm text-[#555] text-center py-6">No threads yet.</p>
) : (
<div className="space-y-3">
{topThreads.map((thread) => (
<div key={thread.id} className="group">
<Link
href={`/forum/thread/${thread.id}`}
className="font-body text-sm text-[#ccc] hover:text-[#3b82f6] transition-colors line-clamp-2 leading-snug"
>
{thread.title}
</Link>
<div className="flex items-center gap-3 mt-1 text-xs font-body text-[#555]">
{thread.forum_categories && (
<Link href={`/forum/${thread.forum_categories.slug}`} className="text-[#3b82f6]/70 hover:text-[#3b82f6]">
{thread.forum_categories.name}
</Link>
)}
<span className={`font-semibold ${(thread.vote_score || 0) >= 0 ? 'text-[#3b82f6]' : 'text-red-400'}`}>
{(thread.vote_score || 0) > 0 ? '+' : ''}{thread.vote_score || 0} pts
</span>
<span>{thread.reply_count || 0} replies</span>
<span>{timeAgo(thread.created_at)}</span>
</div>
</div>
))}
</div>
)}
</div>
{/* Contributions by Category */}
<div className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-5">
<h2 className="font-heading text-sm font-bold text-[#e0e0e0] uppercase tracking-wider mb-4 pb-3 border-b border-[#2a2a2a]">
Contributions by Category
</h2>
{contributionsByCategory.length === 0 ? (
<p className="font-body text-sm text-[#555] text-center py-6">No contributions yet.</p>
) : (
<div className="space-y-3">
{contributionsByCategory.slice(0, 8).map((cat) => (
<div key={cat.slug}>
<div className="flex items-center justify-between mb-1">
<Link
href={`/forum/${cat.slug}`}
className="font-body text-xs font-semibold text-[#aaa] hover:text-[#3b82f6] transition-colors"
>
{cat.name}
</Link>
<span className="font-body text-xs text-[#666]">{cat.count} thread{cat.count !== 1 ? 's' : ''}</span>
</div>
<div className="h-1.5 bg-[#2a2a2a] rounded-full overflow-hidden">
<div
className="h-full bg-[#3b82f6] rounded-full transition-all duration-500"
style={{ width: `${(cat.count / maxCategoryCount) * 100}%` }}
/>
</div>
</div>
))}
</div>
)}
</div>
{/* Recent Activity */}
<div className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-5 lg:col-span-2">
<h2 className="font-heading text-sm font-bold text-[#e0e0e0] uppercase tracking-wider mb-4 pb-3 border-b border-[#2a2a2a]">
Recent Activity
</h2>
{threads.length === 0 && replies.length === 0 ? (
<p className="font-body text-sm text-[#555] text-center py-6">No activity yet.</p>
) : (
<div className="space-y-2">
{[
...threads.slice(0, 5).map((t) => ({ type: 'thread' as const, item: t, date: t.created_at })),
...replies.slice(0, 5).map((r) => ({ type: 'reply' as const, item: r, date: r.created_at })),
]
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
.slice(0, 8)
.map((entry, idx) => (
<div key={idx} className="flex items-start gap-3 py-2 border-b border-[#1e1e1e] last:border-0">
<span className={`font-body text-xs font-bold uppercase tracking-wider px-2 py-0.5 rounded-sm flex-shrink-0 mt-0.5 ${
entry.type === 'thread' ? 'bg-[#1e3a5f] text-[#3b82f6]' : 'bg-[#1a2a1a] text-green-400'
}`}>
{entry.type === 'thread' ? 'Thread' : 'Reply'}
</span>
<div className="flex-1 min-w-0">
{entry.type === 'thread' ? (
<Link
href={`/forum/thread/${(entry.item as ForumThread).id}`}
className="font-body text-sm text-[#ccc] hover:text-[#3b82f6] transition-colors line-clamp-1"
>
{(entry.item as ForumThread).title}
</Link>
) : (
<div>
<Link
href={`/forum/thread/${(entry.item as ForumReply).thread_id}`}
className="font-body text-sm text-[#ccc] hover:text-[#3b82f6] transition-colors line-clamp-1"
>
Re: {(entry.item as ForumReply).forum_threads?.title || 'Thread'}
</Link>
<p className="font-body text-xs text-[#555] line-clamp-1 mt-0.5">
{(entry.item as ForumReply).body}
</p>
</div>
)}
</div>
<span className="font-body text-xs text-[#555] flex-shrink-0">{timeAgo(entry.date)}</span>
</div>
))}
</div>
)}
</div>
</div>
)}
{/* Threads Tab */}
{activeTab === 'threads' && (
<div className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm">
{threads.length === 0 ? (
<div className="p-8 text-center">
<p className="font-body text-sm text-[#555]">No threads posted yet.</p>
</div>
) : (
<div className="divide-y divide-[#2a2a2a]">
{threads.map((thread) => (
<div key={thread.id} className="p-4 hover:bg-[#1e1e1e] transition-colors">
<div className="flex items-start justify-between gap-4">
<div className="flex-1 min-w-0">
<Link
href={`/forum/thread/${thread.id}`}
className="font-body text-sm font-semibold text-[#ccc] hover:text-[#3b82f6] transition-colors line-clamp-2"
>
{thread.title}
</Link>
<p className="font-body text-xs text-[#555] line-clamp-1 mt-1">{thread.body}</p>
<div className="flex flex-wrap items-center gap-3 mt-2 text-xs font-body text-[#555]">
{thread.forum_categories && (
<Link href={`/forum/${thread.forum_categories.slug}`} className="text-[#3b82f6]/70 hover:text-[#3b82f6]">
{thread.forum_categories.name}
</Link>
)}
<span>{thread.reply_count || 0} replies</span>
<span>{thread.view_count || 0} views</span>
<span>{timeAgo(thread.created_at)}</span>
</div>
</div>
<div className="text-right flex-shrink-0">
<div className={`font-heading text-lg font-bold ${(thread.vote_score || 0) >= 0 ? 'text-[#3b82f6]' : 'text-red-400'}`}>
{(thread.vote_score || 0) > 0 ? '+' : ''}{thread.vote_score || 0}
</div>
<div className="font-body text-xs text-[#555]">score</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
)}
{/* Replies Tab */}
{activeTab === 'replies' && (
<div className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm">
{replies.length === 0 ? (
<div className="p-8 text-center">
<p className="font-body text-sm text-[#555]">No replies posted yet.</p>
</div>
) : (
<div className="divide-y divide-[#2a2a2a]">
{replies.map((reply) => (
<div key={reply.id} className="p-4 hover:bg-[#1e1e1e] transition-colors">
<Link
href={`/forum/thread/${reply.thread_id}`}
className="font-body text-xs font-bold uppercase tracking-wider text-[#3b82f6]/70 hover:text-[#3b82f6] transition-colors mb-1 block"
>
Re: {reply.forum_threads?.title || 'Thread'}
</Link>
<p className="font-body text-sm text-[#aaa] line-clamp-3 leading-relaxed">{reply.body}</p>
<div className="flex items-center gap-3 mt-2 text-xs font-body text-[#555]">
<span className={`font-semibold ${(reply.vote_score || 0) >= 0 ? 'text-[#3b82f6]' : 'text-red-400'}`}>
{(reply.vote_score || 0) > 0 ? '+' : ''}{reply.vote_score || 0} pts
</span>
<span>{timeAgo(reply.created_at)}</span>
</div>
</div>
))}
</div>
)}
</div>
)}
{/* Followers Tab */}
{activeTab === 'followers' && (
<FollowUserList
users={followData.followers}
emptyMessage="No followers yet."
currentUserId={currentUserId}
/>
)}
{/* Following Tab */}
{activeTab === 'following' && (
<FollowUserList
users={followData.following}
emptyMessage="Not following anyone yet."
currentUserId={currentUserId}
/>
)}
</div>
</main>
<Footer />
</div>
);
}
function FollowUserList({
users,
emptyMessage,
currentUserId,
}: {
users: FollowUser[];
emptyMessage: string;
currentUserId: string | null;
}) {
if (users.length === 0) {
return (
<div className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-8 text-center">
<p className="font-body text-sm text-[#555]">{emptyMessage}</p>
</div>
);
}
return (
<div className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm divide-y divide-[#2a2a2a]">
{users.map((u) => (
<div key={u.id} className="flex items-center gap-4 p-4 hover:bg-[#1e1e1e] transition-colors">
<Avatar name={u.full_name || 'User'} size="sm" />
<div className="flex-1 min-w-0">
<Link
href={`/profile/${u.id}`}
className="font-body text-sm font-semibold text-[#ccc] hover:text-[#3b82f6] transition-colors"
>
{u.full_name || 'Anonymous User'}
</Link>
<div className="flex items-center gap-2 mt-0.5">
<ReputationBadge rep={u.reputation || 0} />
<span className="font-body text-xs text-[#555]">{(u.reputation || 0).toLocaleString()} rep</span>
</div>
</div>
<div className="flex items-center gap-3 flex-shrink-0">
<span className="font-body text-xs text-[#555]">{timeAgo(u.followed_at)}</span>
{currentUserId && currentUserId !== u.id && (
<Link
href={`/profile/${u.id}`}
className="font-body text-xs font-bold uppercase tracking-wider text-[#888] hover:text-[#3b82f6] border border-[#2a2a2a] hover:border-[#3b82f6] px-3 py-1 rounded-sm transition-colors"
>
View
</Link>
)}
</div>
</div>
))}
</div>
);
}