diff --git a/packages/nextjs/app/explore/Explore.tsx b/packages/nextjs/app/explore/Explore.tsx index 5c4d7e3..431d6b1 100644 --- a/packages/nextjs/app/explore/Explore.tsx +++ b/packages/nextjs/app/explore/Explore.tsx @@ -1,11 +1,8 @@ "use client"; -import { useEffect, useState } from "react"; -import { ErrorComponent } from "./_components/ErrorComponent"; +import { useCallback, useEffect, useRef, useState } from "react"; import { LoadingSpinner } from "./_components/LoadingSpinner"; import { NewsFeed } from "./_components/NewsFeed"; -import { useAccount } from "wagmi"; -import { RainbowKitCustomConnectButton } from "~~/components/scaffold-eth"; import { useScaffoldEventHistory } from "~~/hooks/scaffold-eth"; import { notification } from "~~/utils/scaffold-eth"; import { getMetadataFromIPFS } from "~~/utils/simpleNFT/ipfs-fetch"; @@ -19,14 +16,16 @@ export interface Post extends Partial { } export const Explore = () => { - const { address: isConnected, isConnecting } = useAccount(); - const [listedPosts, setListedPosts] = useState([]); + const [posts, setPosts] = useState([]); const [loading, setLoading] = useState(true); + const [loadingMore, setLoadingMore] = useState(false); + const [page, setPage] = useState(0); + const observer = useRef(null); const { data: createEvents, - isLoading: createIsLoadingEvents, - error: createErrorReadingEvents, + // isLoading: createIsLoadingEvents, + // error: createErrorReadingEvents, } = useScaffoldEventHistory({ contractName: "PunkPosts", eventName: "PostCreated", @@ -34,93 +33,86 @@ export const Explore = () => { watch: true, }); - // const { - // data: deleteEvents, - // isLoading: deleteIsLoadingEvents, - // error: deleteErrorReadingEvents, - // } = useScaffoldEventHistory({ - // contractName: "PunkPosts", - // eventName: "PostDeleted", - // fromBlock: 0n, - // watch: true, - // }); - - useEffect(() => { - const fetchListedNFTs = async () => { + const fetchPosts = useCallback( + async (page: number) => { if (!createEvents) return; - const postsUpdate: Post[] = []; - - for (const event of createEvents || []) { - try { - const { args } = event; - const user = args?.user; - const tokenURI = args?.tokenURI; - - if (!tokenURI) continue; - - const ipfsHash = tokenURI.replace("https://ipfs.io/ipfs/", ""); - const nftMetadata: NFTMetaData = await getMetadataFromIPFS(ipfsHash); - - // Temporary fix for V1 - // Check if the image attribute is valid and does not point to [object Object] - if (nftMetadata.image === "https://ipfs.io/ipfs/[object Object]") { - console.warn(`Skipping post with invalid image URL: ${nftMetadata.image}`); - continue; + setLoadingMore(true); + try { + // Calculate the start and end indices for the current page + const start = (page - 1) * 8; + const end = page * 8; + const eventsToFetch = createEvents.slice(start, end); + + const postsUpdate: Post[] = []; + + for (const event of eventsToFetch) { + try { + const user = event.args?.user; + const tokenURI = event.args?.tokenURI; + + if (!tokenURI) continue; + + const ipfsHash = tokenURI.replace("https://ipfs.io/ipfs/", ""); + const nftMetadata: NFTMetaData = await getMetadataFromIPFS(ipfsHash); + + // Temporary fix for V1 + // Check if the image attribute is valid and does not point to [object Object] + if (nftMetadata.image === "https://ipfs.io/ipfs/[object Object]") { + console.warn(`Skipping post with invalid image URL: ${nftMetadata.image}`); + continue; + } + + postsUpdate.push({ + listingId: undefined, + uri: tokenURI, + user: user || "", + ...nftMetadata, + }); + } catch (e) { + notification.error("Error fetching posts"); + console.error(e); } - - postsUpdate.push({ - listingId: undefined, - uri: tokenURI, - user: user || "", - ...nftMetadata, - }); - } catch (e) { - notification.error("Error fetching posts"); - console.error(e); } - } - setListedPosts(postsUpdate); - }; - - fetchListedNFTs(); - }, [createEvents]); + setPosts(prevPosts => [...prevPosts, ...postsUpdate]); + } catch (error) { + notification.error("Failed to load posts"); + } finally { + setLoadingMore(false); + } + }, + [createEvents], + ); useEffect(() => { - if (listedPosts.length > 0) { - setLoading(false); // Stop loading after Posts are updated - } - }, [listedPosts]); - - // const filteredPosts = listedPosts.filter(Post => { - // return true; - // }); - - if (createIsLoadingEvents) { - return ; - } + setLoading(true); + fetchPosts(page).finally(() => setLoading(false)); + }, [page, fetchPosts]); + + const lastPostElementRef = useCallback( + (node: HTMLDivElement | null) => { + if (loadingMore) return; + if (observer.current) observer.current.disconnect(); + observer.current = new IntersectionObserver(entries => { + if (entries[0].isIntersecting) { + setPage(prevPage => prevPage + 1); + } + }); + if (node) observer.current.observe(node); + }, + [loadingMore], + ); - if (loading) { + if (loading && page === 0) { return ; } - if (createErrorReadingEvents) { - return ; - } - return ( - <> -
{!isConnected || isConnecting ? : ""}
- {listedPosts.length === 0 ? ( -
-
No posts found
-
- ) : loading ? ( - - ) : ( - - )} - +
+ +
+ {loadingMore && } +
); }; diff --git a/packages/nextjs/app/explore/_components/NewsFeed.tsx b/packages/nextjs/app/explore/_components/NewsFeed.tsx index dd13121..f514846 100644 --- a/packages/nextjs/app/explore/_components/NewsFeed.tsx +++ b/packages/nextjs/app/explore/_components/NewsFeed.tsx @@ -1,17 +1,17 @@ import React from "react"; import { Post } from "../Explore"; -import { NFTCard } from "./NFTCard"; +import { PostCard } from "./PostCard"; type NewsFeedProps = { - filteredPosts: Post[]; + posts: Post[]; }; -export const NewsFeed: React.FC = ({ filteredPosts }) => { +export const NewsFeed: React.FC = ({ posts }) => { return (
- {filteredPosts.map(item => ( - + {posts.map(post => ( + ))}
diff --git a/packages/nextjs/app/explore/_components/NFTCard.tsx b/packages/nextjs/app/explore/_components/PostCard.tsx similarity index 89% rename from packages/nextjs/app/explore/_components/NFTCard.tsx rename to packages/nextjs/app/explore/_components/PostCard.tsx index b774171..203e23c 100644 --- a/packages/nextjs/app/explore/_components/NFTCard.tsx +++ b/packages/nextjs/app/explore/_components/PostCard.tsx @@ -14,7 +14,7 @@ export interface Post extends Partial { date?: string; } -export const NFTCard = ({ nft }: { nft: Post }) => { +export const PostCard = ({ post }: { post: Post }) => { const [isModalOpen, setIsModalOpen] = useState(false); const handleOpenModal = () => { @@ -29,11 +29,11 @@ export const NFTCard = ({ nft }: { nft: Post }) => {
{/* Image Section */} - {nft.image && nft.image !== "https://ipfs.io/ipfs/" && ( + {post.image && post.image !== "https://ipfs.io/ipfs/" && (
NFT Image {
-

{nft.description ?? "No description available."}

+

{post.description ?? "No description available."}

- +
@@ -62,7 +62,7 @@ export const NFTCard = ({ nft }: { nft: Post }) => { {isModalOpen && ( NFT Image { const [profilePicture, setProfilePicture] = useState(""); const [website, setWebsite] = useState(""); const [isEditing, setIsEditing] = useState(false); // New state for edit mode - const [listedPosts, setListedPosts] = useState([]); + const [posts, setPosts] = useState([]); const [loading, setLoading] = useState(true); + const [loadingMore, setLoadingMore] = useState(true); + const [page, setPage] = useState(1); // Start from page 1 to get the last post first + const [loadingProfile, setLoadingProfile] = useState(true); + const observer = useRef(null); const { address: connectedAddress } = useAccount(); @@ -50,7 +54,7 @@ const ProfilePage: NextPage = () => { const { data: createEvents, - isLoading: createIsLoadingEvents, + // isLoading: createIsLoadingEvents, error: createErrorReadingEvents, } = useScaffoldEventHistory({ contractName: "PunkPosts", @@ -82,45 +86,78 @@ const ProfilePage: NextPage = () => { } }; - useEffect(() => { - const fetchPosts = async () => { - if (!createEvents) { - // setLoading(false); - return; - } - - const postsUpdate: Post[] = []; - - for (const event of createEvents || []) { - try { - const { args } = event; - const user = args?.user; - const tokenURI = args?.tokenURI; - - if (args?.user !== address) continue; - if (!tokenURI) continue; - - const ipfsHash = tokenURI.replace("https://ipfs.io/ipfs/", ""); - const nftMetadata: NFTMetaData = await getMetadataFromIPFS(ipfsHash); - - postsUpdate.push({ - listingId: undefined, - uri: tokenURI, - user: user || "", - ...nftMetadata, - }); - } catch (e) { - notification.error("Error fetching collection started NFTs"); - console.error(e); + const fetchPosts = useCallback( + async (page: number) => { + if (!createEvents) return; + + setLoadingMore(true); + try { + // Calculate the start and end indices for the current page + const start = (page - 1) * 8; + const end = page * 8; + const eventsToFetch = createEvents.slice(start, end); + + const postsUpdate: Post[] = []; + + for (const event of eventsToFetch) { + try { + const { args } = event; + const user = args?.user; + const tokenURI = args?.tokenURI; + + if (args?.user !== address) continue; + if (!tokenURI) continue; + + const ipfsHash = tokenURI.replace("https://ipfs.io/ipfs/", ""); + const nftMetadata: NFTMetaData = await getMetadataFromIPFS(ipfsHash); + + // Temporary fix for V1 + // Check if the image attribute is valid and does not point to [object Object] + if (nftMetadata.image === "https://ipfs.io/ipfs/[object Object]") { + console.warn(`Skipping post with invalid image URL: ${nftMetadata.image}`); + continue; + } + + postsUpdate.push({ + listingId: undefined, + uri: tokenURI, + user: user || "", + ...nftMetadata, + }); + } catch (e) { + notification.error("Error fetching posts"); + console.error(e); + } } - } - setListedPosts(postsUpdate); - setLoading(false); - }; + setPosts(prevPosts => [...prevPosts, ...postsUpdate]); + } catch (error) { + notification.error("Failed to load posts"); + } finally { + setLoadingMore(false); + } + }, + [createEvents, address], + ); - fetchPosts(); - }, [createEvents, address, connectedAddress]); + useEffect(() => { + setLoading(true); + fetchPosts(page).finally(() => setLoading(false)); + }, [page, fetchPosts]); + + const lastPostElementRef = useCallback( + (node: any) => { + if (loadingMore) return; + if (observer.current) observer.current.disconnect(); + observer.current = new IntersectionObserver(entries => { + if (entries[0].isIntersecting) { + setPage(prevPage => prevPage + 1); + } + }); + if (node) observer.current.observe(node); + }, + [loadingMore], + ); useEffect(() => { if (!isEditing && profileInfo) { @@ -128,19 +165,10 @@ const ProfilePage: NextPage = () => { setBio(profileInfo[1] || ""); setProfilePicture(profileInfo[2] ? profileInfo[2] : defaultProfilePicture); setWebsite(profileInfo[3] || ""); + setLoadingProfile(false); } }, [profileInfo, isEditing]); - useEffect(() => { - if (listedPosts.length > 0) { - setLoading(false); // Stop loading after Posts are updated - } - }, [listedPosts]); - - // const filteredPosts = listedPosts.filter(Post => { - // return true; - // }); - // Ensure the address is available before rendering the component if (!address) { return

Inexistent address, try again...

; @@ -150,7 +178,7 @@ const ProfilePage: NextPage = () => { // return ; // } - if (createIsLoadingEvents) { + if (loading && page === 1) { return ; } @@ -169,89 +197,96 @@ const ProfilePage: NextPage = () => { <>
{/* User Profile Section */} -
- {/* Profile Picture */} -
- + {loadingProfile ? ( +
+
+ +
- {/* User Info Section */} -
+ ) : ( +
+ {/* Profile Picture */} +
+ +
+ {/* User Info Section */} +
+ {isEditing ? ( + + ) : ( + <> +

{username || "Guest user"}

+ + {bio &&

{bio}

} + {website && ( + + {website} + + )} +
+ {address == connectedAddress ? ( + + ) : ( +
+
+
+ )} +
+ + )} +
+ {/* Div to align info in the center */} +
+ {/* User Bio */}{" "} {isEditing ? ( - +
+ <> + + + +
) : ( + <> + )} + {/* Edit/Cancel Button */} + {address === connectedAddress && ( <> -

{username || "Guest user"}

- - {bio &&

{bio}

} - {website && ( - - {website} - + {isEditing ? ( + + ) : ( + + )} + {isEditing && ( +
+ +
)} -
- {address == connectedAddress ? ( - - ) : ( -
-
-
- )} -
)}
- {/* Div to align info in the center */} -
- {/* User Bio */}{" "} - {isEditing ? ( -
- <> - - - -
- ) : ( - <> - )} - {/* Edit/Cancel Button */} - {address === connectedAddress && ( - <> - {isEditing ? ( - - ) : ( - - )} - {isEditing && ( -
- -
- )} - - )} -
+ )}
{/* {loading && } */} - {loading ? ( - - ) : listedPosts.length !== 0 ? ( - - ) : ( -
-
No posts found
-
- )} +
+ +
+ {loadingMore && } +
); }; diff --git a/packages/nextjs/components/Header.tsx b/packages/nextjs/components/Header.tsx index 0c4236c..f3ee2be 100644 --- a/packages/nextjs/components/Header.tsx +++ b/packages/nextjs/components/Header.tsx @@ -110,7 +110,11 @@ export const Header = () => { {/* */} {isSettingsMenuOpen && ( -
+
e.stopPropagation()} + >