diff --git a/README.md b/README.md index 66e11a1..4107275 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ - ✅ **Create ProfileInfo contract for users to register their info** - ✅ **Post creation page** - ✅ **User profile page** -- **View other users profiles** +- ✅ **View other users profiles** ## 🌐 Phase 2 (Social Activity and Indexing) @@ -23,7 +23,7 @@ ## 👥 Phase 3 (Social improvements) - **Individual post pages** for displaying long texts and big images -- Search by address or username +- Search by address or username - **Notification system** - **Accessibility support**: Posts on the website must be [ARIA compliant](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA) diff --git a/packages/nextjs/app/create/_components/ImageUploader.tsx b/packages/nextjs/app/create/_components/ImageUploader.tsx index db66cb4..666c384 100644 --- a/packages/nextjs/app/create/_components/ImageUploader.tsx +++ b/packages/nextjs/app/create/_components/ImageUploader.tsx @@ -2,7 +2,6 @@ import React, { useState } from "react"; import Image from "next/image"; -import imageCompression from "browser-image-compression"; import { notification } from "~~/utils/scaffold-eth"; import { addToIPFS } from "~~/utils/simpleNFT/ipfs-fetch"; @@ -19,24 +18,15 @@ export const ImageUploader: React.FC = ({ image, setUploaded // Handle file drop or selection const handleFileUpload = async (file: File) => { - // Compress the image - const options = { - maxSizeMB: 1, // Maximum size in MB - maxWidthOrHeight: 1920, // Maximum width or height - useWebWorker: true, // Use web worker for faster compression - }; + const reader = new FileReader(); + reader.onloadend = () => setPreviewImage(reader.result as string); // Show preview + reader.readAsDataURL(file); // Convert image to base64 for preview // Upload file to IPFS setLoading(true); const notificationId = notification.loading("Uploading image to IPFS..."); try { - const compressedFile = await imageCompression(file, options); - - const reader = new FileReader(); - reader.onloadend = () => setPreviewImage(reader.result as string); // Show preview - reader.readAsDataURL(compressedFile); // Convert image to base64 for preview - const uploadedImage = await addToIPFS(file, true); // Upload image to IPFS notification.success("Image uploaded to IPFS!"); setUploadedImageIpfsPath(uploadedImage.path); // Store IPFS path for later use diff --git a/packages/nextjs/app/explore/_components/NFTCard.tsx b/packages/nextjs/app/explore/_components/NFTCard.tsx index 415294b..eacba95 100644 --- a/packages/nextjs/app/explore/_components/NFTCard.tsx +++ b/packages/nextjs/app/explore/_components/NFTCard.tsx @@ -1,5 +1,5 @@ import Image from "next/image"; -import { Address } from "~~/components/scaffold-eth"; +import { ProfileAddress } from "./ProfileAddress"; import { NFTMetaData } from "~~/utils/simpleNFT/nftsMetadata"; export interface Collectible extends Partial { @@ -36,7 +36,7 @@ export const NFTCard = ({ nft }: { nft: Collectible }) => {
<> Posted by: -
+
diff --git a/packages/nextjs/app/explore/_components/ProfileAddress.tsx b/packages/nextjs/app/explore/_components/ProfileAddress.tsx new file mode 100644 index 0000000..5b3f75c --- /dev/null +++ b/packages/nextjs/app/explore/_components/ProfileAddress.tsx @@ -0,0 +1,138 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Link from "next/link"; +import { CopyToClipboard } from "react-copy-to-clipboard"; +import { Address as AddressType, getAddress, isAddress } from "viem"; +import { hardhat } from "viem/chains"; +import { normalize } from "viem/ens"; +import { useEnsAvatar, useEnsName } from "wagmi"; +import { CheckCircleIcon, DocumentDuplicateIcon } from "@heroicons/react/24/outline"; +import { BlockieAvatar } from "~~/components/scaffold-eth"; +import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork"; + +type AddressProps = { + address?: AddressType; + disableAddressLink?: boolean; + format?: "short" | "long"; + size?: "xs" | "sm" | "base" | "lg" | "xl" | "2xl" | "3xl"; +}; + +const blockieSizeMap = { + xs: 6, + sm: 7, + base: 8, + lg: 9, + xl: 10, + "2xl": 12, + "3xl": 15, +}; + +/** + * Displays an address (or ENS) with a Blockie image and option to copy address. + */ +export const ProfileAddress = ({ address, disableAddressLink, format, size = "base" }: AddressProps) => { + const [ens, setEns] = useState(); + const [ensAvatar, setEnsAvatar] = useState(); + const [addressCopied, setAddressCopied] = useState(false); + const checkSumAddress = address ? getAddress(address) : undefined; + + const { targetNetwork } = useTargetNetwork(); + + const { data: fetchedEns } = useEnsName({ + address: checkSumAddress, + chainId: 1, + query: { + enabled: isAddress(checkSumAddress ?? ""), + }, + }); + const { data: fetchedEnsAvatar } = useEnsAvatar({ + name: fetchedEns ? normalize(fetchedEns) : undefined, + chainId: 1, + query: { + enabled: Boolean(fetchedEns), + gcTime: 30_000, + }, + }); + + // We need to apply this pattern to avoid Hydration errors. + useEffect(() => { + setEns(fetchedEns); + }, [fetchedEns]); + + useEffect(() => { + setEnsAvatar(fetchedEnsAvatar); + }, [fetchedEnsAvatar]); + + // Skeleton UI + if (!checkSumAddress) { + return ( +
+
+
+
+
+
+ ); + } + + if (!isAddress(checkSumAddress)) { + return Wrong address; + } + + let displayAddress = checkSumAddress?.slice(0, 6) + "..." + checkSumAddress?.slice(-4); + + if (ens) { + displayAddress = ens; + } else if (format === "long") { + displayAddress = checkSumAddress; + } + + return ( +
+
+ +
+ {disableAddressLink ? ( + {displayAddress} + ) : targetNetwork.id === hardhat.id ? ( + + + {displayAddress} + + + ) : ( +
+ + {displayAddress} + +
+ )} + {addressCopied ? ( +
+ ); +}; diff --git a/packages/nextjs/app/myProfile/page.tsx b/packages/nextjs/app/myProfile/page.tsx deleted file mode 100644 index d518cb5..0000000 --- a/packages/nextjs/app/myProfile/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { MyProfile } from "./MyProfile"; -import type { NextPage } from "next"; -import { getMetadata } from "~~/utils/scaffold-eth/getMetadata"; - -export const metadata = getMetadata({ - title: "My Profile", - description: "Built with 🏗 Scaffold-ETH 2", -}); - -const ProfilePage: NextPage = () => { - return ( - <> - - - ); -}; - -export default ProfilePage; diff --git a/packages/nextjs/app/myProfile/MyProfile.tsx b/packages/nextjs/app/profile/[address]/page.tsx similarity index 80% rename from packages/nextjs/app/myProfile/MyProfile.tsx rename to packages/nextjs/app/profile/[address]/page.tsx index 6050503..b360973 100644 --- a/packages/nextjs/app/myProfile/MyProfile.tsx +++ b/packages/nextjs/app/profile/[address]/page.tsx @@ -1,10 +1,11 @@ "use client"; import { useEffect, useState } from "react"; -import { ErrorComponent } from "../explore/_components/ErrorComponent"; -import { LoadingSpinner } from "../explore/_components/LoadingSpinner"; -import { NewsFeed } from "../explore/_components/NewsFeed"; -import { ProfilePictureUpload } from "./_components/ProfilePictureUpload"; +import { usePathname } from "next/navigation"; +import { ErrorComponent } from "../../explore/_components/ErrorComponent"; +import { LoadingSpinner } from "../../explore/_components/LoadingSpinner"; +import { NewsFeed } from "../../explore/_components/NewsFeed"; +import { ProfilePictureUpload } from "../_components/ProfilePictureUpload"; import { NextPage } from "next"; import { useAccount } from "wagmi"; import { PencilIcon } from "@heroicons/react/24/outline"; @@ -22,21 +23,27 @@ export interface Collectible extends Partial { date?: string; } -export const MyProfile: NextPage = () => { +const defaultProfilePicture = "/guest-profile.jpg"; + +const ProfilePage: NextPage = () => { const [name, setName] = useState(""); const [bio, setBio] = useState(""); const [profilePicture, setProfilePicture] = useState(""); const [website, setWebsite] = useState(""); const [isEditing, setIsEditing] = useState(false); // New state for edit mode - const { address: connectedAddress, isConnected, isConnecting } = useAccount(); const [listedCollectibles, setListedCollectibles] = useState([]); const [loading, setLoading] = useState(true); + const { address: connectedAddress, isConnected, isConnecting } = useAccount(); + + const pathname = usePathname(); + const address = pathname.split("/").pop(); + const { data: profileInfo } = useScaffoldReadContract({ contractName: "ProfileInfo", functionName: "profiles", - args: [connectedAddress], + args: [address], watch: true, }); @@ -82,7 +89,7 @@ export const MyProfile: NextPage = () => { const user = args?.user; const tokenURI = args?.tokenURI; - if (args?.user !== connectedAddress) continue; + if (args?.user !== address) continue; if (!tokenURI) continue; const ipfsHash = tokenURI.replace("https://ipfs.io/ipfs/", ""); @@ -104,7 +111,7 @@ export const MyProfile: NextPage = () => { }; fetchListedNFTs(); - }, [createEvents, connectedAddress]); + }, [createEvents, address, connectedAddress]); useEffect(() => { if (!isEditing && profileInfo) { @@ -125,6 +132,11 @@ export const MyProfile: NextPage = () => { // return true; // }); + // Ensure the address is available before rendering the component + if (!address) { + return

Inexistent address, try again...

; + } + if (loading) { return ; } @@ -137,8 +149,6 @@ export const MyProfile: NextPage = () => { return ; } - const defaultProfilePicture = "/guest-profile.jpg"; - // const ensureHttps = (url: string) => { // if (!/^https?:\/\//i.test(url)) { // return `https://${url}`; @@ -171,7 +181,7 @@ export const MyProfile: NextPage = () => { <>

{name || "Guest user"}

-
+

{bio || "no biography available"}

{website && ( @@ -196,24 +206,26 @@ export const MyProfile: NextPage = () => { <> )} {/* Edit/Cancel Button */} - {isEditing ? ( - - ) : ( - - )} - {isEditing ? ( -
- -
- ) : ( - "" + {address === connectedAddress && ( + <> + {isEditing ? ( + + ) : ( + + )} + {isEditing && ( +
+ +
+ )} + )} @@ -238,3 +250,5 @@ export const MyProfile: NextPage = () => { ); }; + +export default ProfilePage; diff --git a/packages/nextjs/app/myProfile/_components/ProfilePictureUpload.tsx b/packages/nextjs/app/profile/_components/ProfilePictureUpload.tsx similarity index 100% rename from packages/nextjs/app/myProfile/_components/ProfilePictureUpload.tsx rename to packages/nextjs/app/profile/_components/ProfilePictureUpload.tsx diff --git a/packages/nextjs/components/Header.tsx b/packages/nextjs/components/Header.tsx index 792947f..9ce7baa 100644 --- a/packages/nextjs/components/Header.tsx +++ b/packages/nextjs/components/Header.tsx @@ -114,7 +114,7 @@ export const Header = () => { {isMenuOpen && isConnected && (
- + My Profile