diff --git a/.gitignore b/.gitignore index 4decb90..9908b88 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,10 @@ dist dist-ssr *.local +# Package manager lock files (project uses pnpm) +package-lock.json +yarn.lock + # Editor directories and files .vscode/* !.vscode/copilot diff --git a/src/Components/FetchCard/FetchCard.tsx b/src/Components/FetchCard/FetchCard.tsx index be08984..67fffdb 100644 --- a/src/Components/FetchCard/FetchCard.tsx +++ b/src/Components/FetchCard/FetchCard.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { HiCalendar } from "react-icons/hi2"; import { queries, utils } from "../../contexts/supabase/supabase"; import { Tables } from "../../contexts/supabase/database"; +import type { stdProfileInfo } from "../../contexts/supabase/supabase"; import PostViewer from "../PostViewer/PostViewer"; import { formatDate } from "../../utils/date"; @@ -9,7 +10,7 @@ interface FetchCardProps { profileId: string; } -interface ProfileData { +interface FetchCardData { profile: Tables<"profiles">; featuredByHandles: string[]; pinnedPost: Tables<"posts"> | null; @@ -19,40 +20,34 @@ interface ProfileData { } export default function FetchCard({ profileId }: FetchCardProps) { - const [profileData, setProfileData] = useState(null); + const [profileData, setProfileData] = useState(null); const [isLoading, setIsLoading] = useState(true); useEffect(() => { async function fetchAllData() { setIsLoading(true); try { + // Get basic profile first to get the handle const profile = await queries.profiles.get(profileId); - const [pinnedPostResult, allPostsResult, featuredProfilesResult, featuredCountResult, categoriesResult] = - await Promise.allSettled([ - profile.pinned_posts?.length - ? queries.posts.get(profile.pinned_posts[0]).catch(() => null) - : Promise.resolve(null), - queries.authors.postsOf(profileId).catch(() => []), - queries.features.byUser(profileId).catch(() => []), - queries.features.byUserCount(profileId).catch(() => 0), - queries.profilesCategories.get(profileId).catch(() => []), - ]); - const allPostsData = allPostsResult.status === "fulfilled" ? allPostsResult.value : []; - const pinnedPostIds = profile.pinned_posts ?? []; - const recentPosts = allPostsData - .filter((post) => post.parent_post === null) - .filter((post) => !pinnedPostIds.includes(post.id)) - .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()) + + // Use our optimized function to get comprehensive profile data + const fullProfileInfo = await queries.views.standardProfileInfo(profile.handle); + + // Get additional data that's not in standardProfileInfo + const featuredProfiles = await queries.features.byUser(profileId).catch(() => []); + + // Transform the data to match the expected format + const recentPosts = fullProfileInfo.mainPosts + .filter(post => post.parent_post === null) .slice(0, 2); setProfileData({ - profile, - pinnedPost: pinnedPostResult.status === "fulfilled" ? pinnedPostResult.value : null, + profile: fullProfileInfo.profile, + pinnedPost: fullProfileInfo.pinnedPosts[0] || null, recentPosts, - featuredByHandles: - featuredProfilesResult.status === "fulfilled" ? featuredProfilesResult.value.map((p) => p.handle) : [], - featuredCount: featuredCountResult.status === "fulfilled" ? featuredCountResult.value : 0, - profileCategories: categoriesResult.status === "fulfilled" ? categoriesResult.value : [], + featuredByHandles: featuredProfiles.map(p => p.handle), + featuredCount: fullProfileInfo.featuredCount, + profileCategories: fullProfileInfo.categories, }); } catch { setProfileData(null); diff --git a/src/Components/PostViewer/PostViewer.tsx b/src/Components/PostViewer/PostViewer.tsx index 7e833f6..7ebac61 100644 --- a/src/Components/PostViewer/PostViewer.tsx +++ b/src/Components/PostViewer/PostViewer.tsx @@ -17,6 +17,7 @@ import { import { useAuth } from "../../contexts/auth/AuthContext"; import { queries, supabase, utils } from "../../contexts/supabase/supabase"; import { Tables } from "../../contexts/supabase/database"; +import { formatDatePost } from "../../utils/date"; import MediaCarousel from "../MediaCarousel/MediaCarousel"; import PostAdd from "../PostAdd/PostAdd"; @@ -75,7 +76,7 @@ export default function PostViewer(props: PostViewerProps) { useEffect(() => { async function fetchPostInfo() { try { - // Si c'est un retweet simple, on récupère les infos du post original + // If it's a simple retweet, fetch info from the original post if (queries.posts.isSimpleRetweet(props.post)) { const originalPost = await queries.posts.getOriginalPost(props.post); if (originalPost) { @@ -88,11 +89,11 @@ export default function PostViewer(props: PostViewerProps) { setLikeCount(originalPostInfo.likesCount); setRetweetCount(originalPostInfo.rtCount); - // Récupérer l'auteur du retweet + // Get the retweet author const retweetAuthors = await queries.authors.ofPost(props.post.id); setRetweetedBy(retweetAuthors[0] || null); - // Pour un retweet simple, l'utilisateur n'est jamais le dernier auteur du post original + // For simple retweets, the user is never the last author of the original post setIsLastAuthor(false); return; } else { @@ -104,7 +105,7 @@ export default function PostViewer(props: PostViewerProps) { setRetweetedBy(null); } - // Pour les posts normaux ou les quote retweets + // For normal posts or quote retweets const postInfo = await queries.views.standardPostInfo(props.post.id); setAuthors(postInfo.profiles); setCategories(postInfo.categories); @@ -195,7 +196,7 @@ export default function PostViewer(props: PostViewerProps) { // Fetch media useEffect(() => { async function fetchMediaUrls() { - // Si c'est un retweet simple, utiliser l'ID du post original + // If it's a simple retweet, use the original post ID let postIdToUse = props.post.id; if (queries.posts.isSimpleRetweet(props.post) && originalPost) { postIdToUse = originalPost.id; @@ -240,7 +241,7 @@ export default function PostViewer(props: PostViewerProps) { } try { - // Pour les retweets simples, vérifier sur le post original + // For simple retweets, check on the original post let targetPostId = props.post.id; if (queries.posts.isSimpleRetweet(props.post) && originalPost) { targetPostId = originalPost.id; @@ -263,24 +264,24 @@ export default function PostViewer(props: PostViewerProps) { void checkUserActions(); }, [props.post.id, props.post, auth.user, originalPost]); - // Récupérer le post cité si c'est un quote tweet + // Fetch quoted post if this is a quote tweet useEffect(() => { async function fetchQuotedPost() { if (queries.posts.isQuoteRetweet(props.post)) { try { - const quoted = await queries.posts.getOriginalPost(props.post); - setQuotedPost(quoted); + const quotedPostData = await queries.posts.getOriginalPost(props.post); + setQuotedPost(quotedPostData); - if (quoted) { + if (quotedPostData) { // Use standardPostInfo for quoted post data - const quotedPostInfo = await queries.views.standardPostInfo(quoted.id); + const quotedPostInfo = await queries.views.standardPostInfo(quotedPostData.id); setQuotedPostAuthors(quotedPostInfo.profiles); setQuotedPostCategories(quotedPostInfo.categories); - // Récupérer les médias du post cité + // Fetch quoted post media try { setLoadingQuotedMedia(true); - const { data, error } = await supabase.storage.from("post-media").list(quoted.id, { + const { data, error } = await supabase.storage.from("post-media").list(quotedPostData.id, { limit: 10, offset: 0, sortBy: { column: "name", order: "asc" }, @@ -289,7 +290,7 @@ export default function PostViewer(props: PostViewerProps) { if (!error && data.length > 0) { const urls = data.map( (file) => - supabase.storage.from("post-media").getPublicUrl(`${quoted.id}/${file.name}`).data.publicUrl, + supabase.storage.from("post-media").getPublicUrl(`${quotedPostData.id}/${file.name}`).data.publicUrl, ); setQuotedPostMediaUrls(urls); } else { @@ -318,16 +319,43 @@ export default function PostViewer(props: PostViewerProps) { void fetchQuotedPost(); }, [props.post]); - const handleReplySuccess = () => { + const handleReplySuccess = async () => { setShowReplyForm(false); - queries.posts - .getChildren(props.post.id) - .then((childPosts) => { - setChildren(childPosts); - }) - .catch(() => { - setChildren([]); + try { + const childPosts = await queries.posts.getChildren(props.post.id); + + // Apply the same logic as fetchChildren for consistency + const childrenWithLikes = await Promise.all( + childPosts.map(async (child) => { + try { + const childInfo = await queries.views.standardPostInfo(child.id); + return { + ...child, + likeCount: childInfo.likesCount, + }; + } catch { + return { + ...child, + likeCount: 0, + }; + } + }), + ); + + // Sort by number of likes (descending), then by creation date (most recent in case of tie) + const sortedChildren = childrenWithLikes.sort((a, b) => { + // First by number of likes (descending) + if (b.likeCount !== a.likeCount) { + return b.likeCount - a.likeCount; + } + // In case of tie, by creation date (most recent first) + return new Date(b.created_at).getTime() - new Date(a.created_at).getTime(); }); + + setChildren(sortedChildren); + } catch { + setChildren([]); + } }; const handlePostClick = (e: React.MouseEvent) => { @@ -374,7 +402,7 @@ export default function PostViewer(props: PostViewerProps) { try { setIsLiking(true); - // Déterminer quel post liker (original pour les retweets simples) + // Determine which post to like (original for simple retweets) let targetPostId = props.post.id; if (queries.posts.isSimpleRetweet(props.post) && originalPost) { targetPostId = originalPost.id; @@ -470,35 +498,6 @@ export default function PostViewer(props: PostViewerProps) { setIsAbandoning(false); } }; - const formatPostDate = (date: Date): string => { - try { - if (isNaN(date.getTime())) { - return "Invalid date"; - } - - const now = new Date(); - const diffInHours = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60)); - - if (diffInHours < 1) { - const diffInMinutes = Math.floor((now.getTime() - date.getTime()) / (1000 * 60)); - return diffInMinutes < 1 ? "now" : `${diffInMinutes.toString()}m`; - } else if (diffInHours < 24) { - return `${diffInHours.toString()}h`; - } else if (diffInHours < 24 * 7) { - const diffInDays = Math.floor(diffInHours / 24); - return `${diffInDays.toString()}d`; - } else { - return date.toLocaleDateString("en-US", { - month: "short", - day: "numeric", - year: date.getFullYear() !== now.getFullYear() ? "numeric" : undefined, - }); - } - } catch { - return "Invalid date"; - } - }; - return (
{/* Parent posts */} @@ -543,14 +542,14 @@ export default function PostViewer(props: PostViewerProps) { onClick={handlePostClick} > {" "} - {/* Indicateur de retweet simple */} + {/* Simple retweet indicator */} {queries.posts.isSimpleRetweet(props.post) && retweetedBy && (
{retweetedBy.handle} retweeted
)} - {/* Menu burger */} + {/* Dropdown menu */}
{quotedPost.body && (
@@ -891,15 +890,15 @@ export default function PostViewer(props: PostViewerProps) { void (async () => { if (hasRetweeted) { try { - // Déterminer quel post utiliser pour chercher les retweets + // Determine which post to use for finding retweets let targetPostId = props.post.id; if (queries.posts.isSimpleRetweet(props.post) && originalPost) { targetPostId = originalPost.id; } - // Trouver le retweet de l'utilisateur et l'abandonner + // Find the user's retweet and abandon it const retweets = await queries.posts.getRetweetsOf(targetPostId); - // Rechercher le retweet de l'utilisateur actuel + // Search for the current user's retweet let userRetweetId: string | null = null; for (const rt of retweets) { const retweetAuthors = await queries.authors.ofPost(rt.id); @@ -927,7 +926,7 @@ export default function PostViewer(props: PostViewerProps) { alert(`Error: ${errorMessage}`); } } else { - // Ouvrir le dialog pour retweeter + // Open the retweet dialog const modal = document.getElementById(`retweet-modal-${props.post.id}`) as HTMLDialogElement; modal.showModal(); } diff --git a/src/contexts/supabase/supabase.ts b/src/contexts/supabase/supabase.ts index bba5850..47a1479 100644 --- a/src/contexts/supabase/supabase.ts +++ b/src/contexts/supabase/supabase.ts @@ -21,6 +21,20 @@ interface stdPostInfo { rtCount: number; } +interface batchPostInfo { + postsInfo: Record; +} + +interface stdProfileInfo { + profile: Tables<"profiles">; + categories: Tables<"categories">[]; + pinnedPosts: Tables<"posts">[]; + mainPosts: Tables<"posts">[]; + allPosts: Tables<"posts">[]; + featuredCount: number; + repliesCount: number; +} + interface PostSearchQuery { has_text?: string[]; has_authors?: string[]; @@ -888,6 +902,112 @@ const queries = { rtCount: req.data.rt_of?.count ?? 0, }; }, + + standardProfileInfo: async function (handle: string): Promise { + try { + // First, get the profile + const profile = await queries.profiles.getByHandle(handle); + + // Then get all related data in parallel + const [ + categoriesResult, + pinnedPostsResult, + allPostsResult, + simpleRetweetsResult, + featuredCountResult + ] = await Promise.allSettled([ + queries.profilesCategories.get(profile.id), + profile.pinned_posts?.length + ? Promise.all(profile.pinned_posts.map(postId => queries.posts.get(postId).catch(() => null))) + : Promise.resolve([]), + queries.authors.postsOf(profile.id), + queries.authors.simpleRetweetsOf(profile.id), + queries.features.byUserCount(profile.id) + ]); + + const categories = categoriesResult.status === "fulfilled" ? categoriesResult.value : []; + const pinnedPosts = (pinnedPostsResult.status === "fulfilled" ? pinnedPostsResult.value : []) + .filter(Boolean) as Tables<"posts">[]; + const allPosts = allPostsResult.status === "fulfilled" ? allPostsResult.value : []; + const simpleRetweets = simpleRetweetsResult.status === "fulfilled" ? simpleRetweetsResult.value : []; + const featuredCount = featuredCountResult.status === "fulfilled" ? featuredCountResult.value : 0; + + const pinnedPostIds = profile.pinned_posts ?? []; + + // Combine regular posts with simple retweets for main posts display + const mainPostsCombined = [...allPosts, ...simpleRetweets]; + const mainPosts = mainPostsCombined + .filter(post => !pinnedPostIds.includes(post.id)) + .filter(post => post.parent_post === null) + .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); + + // All posts exclude pinned posts but include replies + const filteredAllPosts = allPosts + .filter(post => !pinnedPostIds.includes(post.id)) + .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); + + const repliesCount = filteredAllPosts.filter(post => post.parent_post !== null).length; + + return { + profile, + categories, + pinnedPosts: pinnedPosts.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()), + mainPosts, + allPosts: filteredAllPosts, + featuredCount, + repliesCount + }; + } catch (error) { + throw new Error(`Failed to load profile info: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + }, + + batchPostInfo: async function (postIds: string[]): Promise { + try { + if (postIds.length === 0) { + return { postsInfo: {} }; + } + + // Fetch all posts with related data in a single query using IN clause + const req = await supabase + .from("posts") + .select( + ` + *, + postsCategories:postsCategories( + categories:categories(*) + ), + authors:authors( + profiles:profiles(*) + ), + likes:likes(count), + rt_of:posts!rt_of(count) + `, + ) + .in("id", postIds); + + if (req.error) throw new Error(req.error.message); + + const postsInfo: Record = {}; + + for (const postData of req.data) { + const categories = postData.postsCategories.map((pc) => pc.categories as Tables<"categories">); + const profiles = postData.authors.map((pc) => pc.profiles as Tables<"profiles">); + + postsInfo[postData.id] = { + post: postData as Tables<"posts">, + categories, + profiles, + likesCount: postData.likes[0]?.count ?? 0, + rtCount: postData.rt_of?.count ?? 0, + }; + } + + return { postsInfo }; + } catch (error) { + throw new Error(`Failed to batch load post info: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + }, }, feed: { @@ -959,4 +1079,4 @@ const utils = { }; export { supabase, queries, utils }; -export type { PostSearchQuery, ProfileSearchQuery, postWithCategories, stdPostInfo }; +export type { PostSearchQuery, ProfileSearchQuery, postWithCategories, stdPostInfo, stdProfileInfo, batchPostInfo }; diff --git a/src/pages/ProfileViewer/ProfileViewer.tsx b/src/pages/ProfileViewer/ProfileViewer.tsx index b46f2c8..b5b9377 100644 --- a/src/pages/ProfileViewer/ProfileViewer.tsx +++ b/src/pages/ProfileViewer/ProfileViewer.tsx @@ -11,6 +11,7 @@ import { import { useAuth } from "../../contexts/auth/AuthContext"; import { queries, utils } from "../../contexts/supabase/supabase"; import { Tables } from "../../contexts/supabase/database"; +import type { stdProfileInfo } from "../../contexts/supabase/supabase"; import TopBar from "../../layouts/TopBar/TopBar"; import PostViewer from "../../Components/PostViewer/PostViewer"; @@ -25,13 +26,7 @@ const ProfileViewer = () => { const navigate = useNavigate(); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - const [profile, setProfile] = useState | null>(null); - const [pinnedPosts, setPinnedPosts] = useState[]>([]); - const [mainPosts, setMainPosts] = useState[]>([]); - const [allPosts, setAllPosts] = useState[]>([]); - const [repliesCount, setRepliesCount] = useState(0); - const [featuredCount, setFeaturedCount] = useState(0); - const [profileCategories, setProfileCategories] = useState[]>([]); + const [profileInfo, setProfileInfo] = useState(null); const [isFollowing, setIsFollowing] = useState(false); const [isFeaturing, setIsFeaturing] = useState(false); const [activeTab, setActiveTab] = useState<"main" | "all">("main"); @@ -40,12 +35,7 @@ const ProfileViewer = () => { useEffect(() => { setIsLoading(true); setError(null); - setProfile(null); - setPinnedPosts([]); - setMainPosts([]); - setAllPosts([]); - setRepliesCount(0); - setProfileCategories([]); + setProfileInfo(null); async function loadProfileData() { if (!handle) { @@ -53,136 +43,46 @@ const ProfileViewer = () => { setIsLoading(false); return; } + try { - const profileData = await queries.profiles.getByHandle(handle); - setProfile(profileData); // Step 2: If profile has pinned posts, fetch them - if (profileData.pinned_posts && profileData.pinned_posts.length > 0) { - const pinnedPostPromises = profileData.pinned_posts.map((postId) => - queries.posts.get(postId).catch(() => null), - ); - const pinnedPostsData = await Promise.all(pinnedPostPromises); - const filteredPinnedPosts = pinnedPostsData.filter(Boolean) as Tables<"posts">[]; - // Tri par date directement ici - const sortedPinnedPosts = filteredPinnedPosts.sort( - (a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime(), - ); - setPinnedPosts(sortedPinnedPosts); - } - if (profileData.id) { - try { - // Pour l'onglet "main" : récupère les posts normaux ET les retweets simples - const authorPosts = await queries.authors.postsOf(profileData.id); - const simpleRetweets = await queries.authors.simpleRetweetsOf(profileData.id); - - const pinnedPostIds = profileData.pinned_posts ?? []; - - // Pour l'onglet "Posts" (main) : combine posts normaux et retweets simples - const mainPostsCombined = [...authorPosts, ...simpleRetweets]; - const filteredMainPosts = mainPostsCombined.filter((post) => !pinnedPostIds.includes(post.id)); - - // Pour l'onglet "All posts" : utilise seulement les posts normaux (avec conversations) - const filteredAllPosts = authorPosts.filter((post) => !pinnedPostIds.includes(post.id)); - - // Separate main posts (without parent) from main posts combined and sort by date - const mainPostsData = filteredMainPosts - .filter((post) => post.parent_post === null) - .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); - - // For the "all" tab, keep all posts (including replies) and sort by date - const allPostsData = filteredAllPosts.sort( - (a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime(), - ); - - setMainPosts(mainPostsData); - setAllPosts(allPostsData); - setRepliesCount(allPostsData.filter((post) => post.parent_post !== null).length); - } catch { - // Continue execution even if posts fetching fails - } - } - - const featuredCount = await queries.features.byUserCount(profileData.id); - setFeaturedCount(featuredCount); - - try { - const categories = await queries.profilesCategories.get(profileData.id); - setProfileCategories(categories); - } catch { - setProfileCategories([]); - } + // Use the new optimized function that gets all data efficiently + const profileData = await queries.views.standardProfileInfo(handle); + setProfileInfo(profileData); } catch (err) { setError(err instanceof Error ? err.message : "Failed to load profile"); } finally { setIsLoading(false); } } + void loadProfileData(); }, [handle]); useEffect(() => { async function checkFollowingStatus() { - if (!profile || !auth.user) return; - const isFollowing = await queries.follows.doesXFollowY(auth.user.id, profile.id); + if (!profileInfo?.profile || !auth.user) return; + const isFollowing = await queries.follows.doesXFollowY(auth.user.id, profileInfo.profile.id); setIsFollowing(isFollowing); } void checkFollowingStatus(); - }, [profile, auth.user]); + }, [profileInfo?.profile, auth.user]); useEffect(() => { async function checkFeaturingStatus() { - if (!profile || !auth.user) return; - const isFeaturing = await queries.features.doesXfeatureY(auth.user.id, profile.id); + if (!profileInfo?.profile || !auth.user) return; + const isFeaturing = await queries.features.doesXfeatureY(auth.user.id, profileInfo.profile.id); setIsFeaturing(isFeaturing); } void checkFeaturingStatus(); - }, [profile, auth.user]); + }, [profileInfo?.profile, auth.user]); const handlePinUpdate = async () => { - if (!profile) return; + if (!profileInfo?.profile) return; try { - // Reload profile data to get updated pinned posts - const updatedProfile = await queries.profiles.getByHandle(profile.handle); - setProfile(updatedProfile); - - // Reload pinned posts - if (updatedProfile.pinned_posts && updatedProfile.pinned_posts.length > 0) { - const pinnedPostPromises = updatedProfile.pinned_posts.map((postId) => - queries.posts.get(postId).catch(() => null), - ); - const pinnedPostsData = await Promise.all(pinnedPostPromises); - const filteredPinnedPosts = pinnedPostsData.filter(Boolean) as Tables<"posts">[]; - // Tri par date directement ici - const sortedPinnedPosts = filteredPinnedPosts.sort( - (a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime(), - ); - setPinnedPosts(sortedPinnedPosts); - } else { - setPinnedPosts([]); - } // Reload all posts using the new approach - const authorPosts = await queries.authors.postsOf(updatedProfile.id); - const simpleRetweets = await queries.authors.simpleRetweetsOf(updatedProfile.id); - const pinnedPostIds = updatedProfile.pinned_posts ?? []; - - // For main tab: combine regular posts and simple retweets - const mainPostsCombined = [...authorPosts, ...simpleRetweets]; - const filteredMainPosts = mainPostsCombined.filter((post) => !pinnedPostIds.includes(post.id)); - - // For all tab: only regular posts (with conversations) - const filteredAllPosts = authorPosts.filter((post) => !pinnedPostIds.includes(post.id)); - - // Main posts: filter main posts only and sort by date - const mainPostsData = filteredMainPosts - .filter((post) => post.parent_post === null) - .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); - - // All posts: keep all posts (including replies) and sort by date - const allPostsData = filteredAllPosts.sort( - (a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime(), - ); - setMainPosts(mainPostsData); - setAllPosts(allPostsData); - setRepliesCount(allPostsData.filter((post) => post.parent_post !== null).length); + // Reload profile data efficiently using our optimized function + const updatedProfileInfo = await queries.views.standardProfileInfo(profileInfo.profile.handle); + setProfileInfo(updatedProfileInfo); } catch { // Error handling without logging } @@ -190,8 +90,9 @@ const ProfileViewer = () => { if (isLoading) return ; if (error) return
Error: {error}
; - if (!profile) return
Profile not found
; + if (!profileInfo) return
Profile not found
; + const { profile, pinnedPosts, mainPosts, allPosts, featuredCount, repliesCount, categories: profileCategories } = profileInfo; const profileCreationDate = new Date(profile.created_at); return (