import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import Layout from "@/components/Layout"; import LoadingScreen from "@/components/LoadingScreen"; import PostComposer from "@/components/social/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 "@/hooks/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, } from "lucide-react"; 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; } 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"; } { 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"), }; } catch { return { text: content, mediaUrl: null, mediaType: "none" }; } } export default function Feed() { 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 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, }; }), [], ); const fetchFeed = useCallback(async () => { setIsLoading(true); try { // Add timeout to prevent indefinite hanging (30 seconds) const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 30000); try { // Parallelize posts and following fetch with timeout 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(() => { 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 filteredItems = useMemo(() => { if (activeFilter === "following") { return items.filter( (item) => isFollowingAuthor(item.authorId) || item.authorId === user?.id, ); } if (activeFilter === "trending") { return [...items].sort( (a, b) => b.likes + b.comments - (a.likes + a.comments), ); } return items; }, [activeFilter, isFollowingAuthor, items, 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 ( ); } return (

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 }) => ( ))}

Share something new

Post updates, showcase progress, or spark a conversation with the community.

fetchFeed()} suggestedTags={trendingTopics .map((t) => t.topic.replace(/^#/, "")) .slice(0, 8)} />
Your post is shared instantly with followers and the broader community.
{filteredItems.length === 0 ? ( No stories found Try switching filters or follow more creators to personalize your feed. ) : (
{filteredItems.map((item) => ( ))}
)}
); }