import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import Layout from "@/components/Layout"; import LoadingScreen from "@/components/LoadingScreen"; import PostComposer from "@/components/feed/PostComposer"; import { FeedItemCard } from "@/components/social/FeedItemCard"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Separator } from "@/components/ui/separator"; import { useToast } from "@/components/ui/use-toast"; import { useAuth } from "@/contexts/AuthContext"; import { aethexSocialService } from "@/lib/aethex-social-service"; import { cn } from "@/lib/utils"; import { normalizeErrorMessage } from "@/lib/error-utils"; import { communityService, realtimeService } from "@/lib/supabase-service"; import { ArrowUpRight, Flame, RotateCcw, Sparkles, TrendingUp, Users, Zap, Gamepad2, Briefcase, BookOpen, Network, Shield, } from "lucide-react"; const API_BASE = import.meta.env.VITE_API_BASE || ""; export type ArmType = | "labs" | "gameforge" | "corp" | "foundation" | "devlink" | "nexus" | "staff"; const ARMS: { id: ArmType; label: string; icon: any; color: string }[] = [ { id: "labs", label: "Labs", icon: Zap, color: "text-yellow-400" }, { id: "gameforge", label: "GameForge", icon: Gamepad2, color: "text-green-400", }, { id: "corp", label: "Corp", icon: Briefcase, color: "text-blue-400" }, { id: "foundation", label: "Foundation", icon: BookOpen, color: "text-red-400", }, { id: "devlink", label: "Dev-Link", icon: Network, color: "text-cyan-400" }, { id: "nexus", label: "Nexus", icon: Sparkles, color: "text-purple-400" }, { id: "staff", label: "Staff", icon: Shield, color: "text-indigo-400" }, ]; export interface FeedItem { id: string; authorId: string; authorName: string; authorAvatar?: string | null; caption?: string; mediaUrl?: string | null; mediaType: "video" | "image" | "none"; likes: number; comments: number; arm?: ArmType; source?: "discord" | "web" | null; discordChannelName?: string | null; discordAuthorTag?: string | null; } interface TrendingTopic { topic: string; count: number; } interface CreatorSummary { id: string; name: string; avatar?: string | null; likes: number; posts: number; } function parseContent(content: string): { text?: string; mediaUrl?: string | null; mediaType: "video" | "image" | "none"; source?: "discord" | "web" | null; discordChannelName?: string | null; discordAuthorTag?: string | null; } { try { const obj = JSON.parse(content || "{}"); return { text: obj.text || content, mediaUrl: obj.mediaUrl || null, mediaType: obj.mediaType || (obj.mediaUrl ? /(mp4|webm|mov)$/i.test(obj.mediaUrl) ? "video" : "image" : "none"), source: obj.source || null, discordChannelName: obj.discord_channel_name || obj.discord_channel || null, discordAuthorTag: obj.discord_author_tag || null, }; } catch { return { text: content, mediaUrl: null, mediaType: "none", source: null }; } } interface FeedProps { embedded?: boolean; } export default function Feed({ embedded = false }: FeedProps) { const { user, loading } = useAuth(); const { toast } = useToast(); const composerRef = useRef(null); const [isLoading, setIsLoading] = useState(true); const [following, setFollowing] = useState([]); const [items, setItems] = useState([]); const [activeFilter, setActiveFilter] = useState< "all" | "following" | "trending" >("all"); const [selectedArms, setSelectedArms] = useState([]); const [showPostComposer, setShowPostComposer] = useState(false); const [followedArms, setFollowedArms] = useState([]); const [showArmFollowManager, setShowArmFollowManager] = useState(false); const mapPostsToFeedItems = useCallback( (source: any[]): FeedItem[] => (Array.isArray(source) ? source : []).map((p: any) => { const meta = parseContent(p.content); const author = p.user_profiles || {}; return { id: p.id, authorId: p.author_id, authorName: author.full_name || author.username || "Community member", authorAvatar: author.avatar_url, caption: meta.text, mediaUrl: meta.mediaUrl, mediaType: meta.mediaType, likes: p.likes_count ?? 0, comments: p.comments_count ?? 0, arm: p.arm_affiliation || "labs", source: meta.source, discordChannelName: meta.discordChannelName, discordAuthorTag: meta.discordAuthorTag, }; }), [], ); const fetchFeed = useCallback(async () => { setIsLoading(true); try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 30000); try { const [posts, flw] = await Promise.race([ Promise.all([ communityService.getPosts(30), user?.id ? aethexSocialService.getFollowing(user.id) : Promise.resolve([]), ]), new Promise((_, reject) => setTimeout( () => reject(new Error("Feed loading timeout - try refreshing")), 28000, ), ), ]); clearTimeout(timeoutId); setFollowing(Array.isArray(flw) ? flw : []); let mapped = mapPostsToFeedItems(posts); setItems(mapped); } catch (timeoutError) { clearTimeout(timeoutId); throw timeoutError; } } catch (error) { console.error("Failed to load feed", error); toast({ variant: "destructive", title: "Failed to load feed", description: normalizeErrorMessage(error), }); setItems([]); } finally { setIsLoading(false); } }, [mapPostsToFeedItems, toast, user?.id]); useEffect(() => { // Initialize with all arms if none selected if (selectedArms.length === 0) { setSelectedArms(ARMS.map((a) => a.id)); } // Load user's followed arms if (user?.id) { loadFollowedArms(); } }, [user?.id]); const loadFollowedArms = async () => { if (!user?.id) return; try { const response = await fetch(`/api/user/arm-follows?user_id=${user.id}`); if (response.ok) { const { arms } = await response.json(); setFollowedArms(arms || []); } } catch (error) { console.warn("Failed to load followed arms:", error); } }; const toggleFollowArm = async (arm: ArmType) => { if (!user?.id) { toast({ description: "Please sign in to manage arm follows." }); return; } try { if (followedArms.includes(arm)) { // Unfollow await fetch(`/api/user/arm-follows?user_id=${user.id}`, { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ arm_affiliation: arm }), }); setFollowedArms((state) => state.filter((a) => a !== arm)); toast({ description: `Unfollowed ${ARMS.find((a) => a.id === arm)?.label}`, }); } else { // Follow await fetch(`/api/user/arm-follows?user_id=${user.id}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ arm_affiliation: arm }), }); setFollowedArms((state) => Array.from(new Set([...state, arm]))); toast({ description: `Following ${ARMS.find((a) => a.id === arm)?.label}!`, }); } } catch (error) { console.error("Failed to update arm follow:", error); toast({ variant: "destructive", description: "Failed to update preference", }); } }; useEffect(() => { fetchFeed(); let cleanup: (() => void) | undefined; try { const subscription = realtimeService.subscribeToCommunityPosts(() => { fetchFeed(); }); cleanup = () => { try { subscription.unsubscribe?.(); } catch (error) { console.warn("Unable to unsubscribe from community posts", error); } }; } catch (error) { console.warn("Realtime subscription unavailable", error); } return () => { cleanup?.(); }; }, [fetchFeed]); const isFollowingAuthor = useCallback( (id: string) => following.includes(id), [following], ); const toggleFollow = useCallback( async (targetId: string) => { if (!user) { toast({ description: "Please sign in to manage follows." }); return; } try { if (isFollowingAuthor(targetId)) { await aethexSocialService.unfollowUser(user.id, targetId); setFollowing((state) => state.filter((value) => value !== targetId)); } else { await aethexSocialService.followUser(user.id, targetId); setFollowing((state) => Array.from(new Set([...state, targetId]))); } } catch (error: any) { toast({ variant: "destructive", title: "Action failed", description: error?.message || "Try again in a moment.", }); } }, [isFollowingAuthor, toast, user], ); const handleShare = useCallback( async (id: string) => { const url = `${location.origin}/feed#post-${id}`; try { if ((navigator as any).share) { await (navigator as any).share({ title: "AeThex", text: "Check out this post on AeThex", url, }); } else if (typeof window !== "undefined") { const intent = `https://twitter.com/intent/tweet?text=${encodeURIComponent( "Check out this post on AeThex", )}&url=${encodeURIComponent(url)}`; window.open(intent, "_blank", "noopener,noreferrer"); await navigator.clipboard.writeText(url).catch(() => undefined); toast({ description: "Share link opened (copied to clipboard)" }); } } catch (error) { console.warn("Share cancelled", error); } }, [toast], ); const handleLike = useCallback( async (postId: string) => { if (!user?.id) { toast({ description: "Please sign in to like posts." }); return; } try { const newCount = await communityService.likePost(postId, user.id); setItems((prev) => prev.map((it) => it.id === postId && typeof newCount === "number" ? { ...it, likes: newCount } : it, ), ); } catch (e) { console.warn("Like failed", e); } }, [toast, user?.id], ); const handleComment = useCallback((postId: string) => { setItems((prev) => prev.map((it) => it.id === postId ? { ...it, comments: it.comments + 1 } : it, ), ); }, []); const handlePostSuccess = useCallback(() => { setShowPostComposer(false); fetchFeed(); }, [fetchFeed]); const filteredItems = useMemo(() => { let filtered = items.filter((item) => selectedArms.includes(item.arm || "labs"), ); if (activeFilter === "following") { filtered = filtered.filter( (item) => isFollowingAuthor(item.authorId) || item.authorId === user?.id, ); } if (activeFilter === "trending") { filtered = [...filtered].sort( (a, b) => b.likes + b.comments - (a.likes + a.comments), ); } return filtered; }, [activeFilter, isFollowingAuthor, items, selectedArms, user?.id]); const trendingTopics = useMemo(() => { const counts = new Map(); items.forEach((item) => { const matches = item.caption?.match(/#[\p{L}0-9_]+/gu) ?? []; matches.forEach((tag) => { counts.set(tag, (counts.get(tag) ?? 0) + 1); }); }); return Array.from(counts.entries()) .map(([topic, count]) => ({ topic, count })) .sort((a, b) => b.count - a.count) .slice(0, 5); }, [items]); const topCreators = useMemo(() => { const map = new Map(); items.forEach((item) => { const existing = map.get(item.authorId) ?? { id: item.authorId, name: item.authorName, avatar: item.authorAvatar, likes: 0, posts: 0, }; existing.likes += item.likes; existing.posts += 1; if (!existing.avatar) { existing.avatar = item.authorAvatar; } map.set(item.authorId, existing); }); return Array.from(map.values()) .sort((a, b) => { if (b.likes === a.likes) return b.posts - a.posts; return b.likes - a.likes; }) .slice(0, 5); }, [items]); const suggestedCreators = useMemo(() => { return topCreators.filter( (creator) => creator.id !== user?.id && !isFollowingAuthor(creator.id), ); }, [isFollowingAuthor, topCreators, user?.id]); const totalEngagement = useMemo( () => items.reduce( (acc, item) => acc + (Number(item.likes) || 0) + (Number(item.comments) || 0), 0, ), [items], ); const averageEngagement = useMemo(() => { if (!items.length) return 0; return Math.round(totalEngagement / items.length); }, [items.length, totalEngagement]); const handleScrollToComposer = useCallback(() => { composerRef.current?.scrollIntoView({ behavior: "smooth", block: "center", }); }, []); const handleManualRefresh = useCallback(() => { fetchFeed(); }, [fetchFeed]); if (loading || (isLoading && items.length === 0)) { return ( ); } const content = (

Community Pulse

Discover new creations, amplify your voice, and engage with the AeThex community in real time.

Live updates enabled
{[ { key: "all" as const, label: "All stories", icon: Sparkles, description: "Latest community activity", }, { key: "following" as const, label: "Following", icon: Users, description: "People you follow", }, { key: "trending" as const, label: "Trending", icon: Flame, description: "Most engagement", }, ].map(({ key, label, icon: Icon, description }) => ( ))}
Filter by Arms
{ARMS.map((arm) => ( ))}
{showArmFollowManager && user?.id && (

✨ Follow arms to personalize your feed. Only posts from followed arms will appear in your "Following" tab.

{ARMS.map((arm) => ( ))}
)}

Share something new

Post updates or spark a conversation

Your post is shared instantly with followers and the broader community. Posts shared instantly
{/* Mobile Stats Card - Only visible on mobile */}

Stories

{items.length}

Following

{following.length}

{filteredItems.length === 0 ? ( No stories found Try switching filters or follow more creators to personalize your feed. ) : (
{filteredItems.map((item) => ( ))}
)}
); if (embedded) { return content; } return {content}; }