Skip to content

Commit

Permalink
Now you can see other's profiles, and some other changes
Browse files Browse the repository at this point in the history
  • Loading branch information
luloxi committed Sep 28, 2024
1 parent fe7291a commit 5963c89
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 66 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)

Expand Down
16 changes: 3 additions & 13 deletions packages/nextjs/app/create/_components/ImageUploader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -19,24 +18,15 @@ export const ImageUploader: React.FC<ImageUploaderProps> = ({ 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
Expand Down
4 changes: 2 additions & 2 deletions packages/nextjs/app/explore/_components/NFTCard.tsx
Original file line number Diff line number Diff line change
@@ -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<NFTMetaData> {
Expand Down Expand Up @@ -36,7 +36,7 @@ export const NFTCard = ({ nft }: { nft: Collectible }) => {
<div className="flex space-x-3 mt-1 items-center">
<>
<span className="text-lg font-semibold">Posted by: </span>
<Address address={nft.user} />
<ProfileAddress address={nft.user} />
</>
</div>
</div>
Expand Down
138 changes: 138 additions & 0 deletions packages/nextjs/app/explore/_components/ProfileAddress.tsx
Original file line number Diff line number Diff line change
@@ -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<string | null>();
const [ensAvatar, setEnsAvatar] = useState<string | null>();
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 (
<div className="animate-pulse flex space-x-4">
<div className="rounded-md bg-slate-300 h-6 w-6"></div>
<div className="flex items-center space-y-6">
<div className="h-2 w-28 bg-slate-300 rounded"></div>
</div>
</div>
);
}

if (!isAddress(checkSumAddress)) {
return <span className="text-error">Wrong address</span>;
}

let displayAddress = checkSumAddress?.slice(0, 6) + "..." + checkSumAddress?.slice(-4);

if (ens) {
displayAddress = ens;
} else if (format === "long") {
displayAddress = checkSumAddress;
}

return (
<div className="flex items-center flex-shrink-0">
<div className="flex-shrink-0">
<BlockieAvatar
address={checkSumAddress}
ensImage={ensAvatar}
size={(blockieSizeMap[size] * 24) / blockieSizeMap["base"]}
/>
</div>
{disableAddressLink ? (
<span className={`ml-1.5 text-${size} font-normal`}>{displayAddress}</span>
) : targetNetwork.id === hardhat.id ? (
<span className={`ml-1.5 text-${size} font-normal`}>
<Link href={`/profile/${checkSumAddress}`} passHref>
{displayAddress}
</Link>
</span>
) : (
<div className={`ml-1.5 text-${size} font-normal`}>
<Link href={`/profile/${checkSumAddress}`} passHref>
{displayAddress}
</Link>
</div>
)}
{addressCopied ? (
<CheckCircleIcon
className="ml-1.5 text-xl font-normal text-sky-600 h-5 w-5 cursor-pointer flex-shrink-0"
aria-hidden="true"
/>
) : (
<CopyToClipboard
text={checkSumAddress}
onCopy={() => {
setAddressCopied(true);
setTimeout(() => {
setAddressCopied(false);
}, 800);
}}
>
<DocumentDuplicateIcon
className="ml-1.5 text-xl font-normal text-sky-600 h-5 w-5 cursor-pointer flex-shrink-0"
aria-hidden="true"
/>
</CopyToClipboard>
)}
</div>
);
};
18 changes: 0 additions & 18 deletions packages/nextjs/app/myProfile/page.tsx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -22,21 +23,27 @@ export interface Collectible extends Partial<NFTMetaData> {
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<string>("");
const [website, setWebsite] = useState("");
const [isEditing, setIsEditing] = useState(false); // New state for edit mode

const { address: connectedAddress, isConnected, isConnecting } = useAccount();
const [listedCollectibles, setListedCollectibles] = useState<Collectible[]>([]);
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,
});

Expand Down Expand Up @@ -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/", "");
Expand All @@ -104,7 +111,7 @@ export const MyProfile: NextPage = () => {
};

fetchListedNFTs();
}, [createEvents, connectedAddress]);
}, [createEvents, address, connectedAddress]);

useEffect(() => {
if (!isEditing && profileInfo) {
Expand All @@ -125,6 +132,11 @@ export const MyProfile: NextPage = () => {
// return true;
// });

// Ensure the address is available before rendering the component
if (!address) {
return <p>Inexistent address, try again...</p>;
}

if (loading) {
return <LoadingSpinner />;
}
Expand All @@ -137,8 +149,6 @@ export const MyProfile: NextPage = () => {
return <ErrorComponent message={createErrorReadingEvents?.message || "Error loading events"} />;
}

const defaultProfilePicture = "/guest-profile.jpg";

// const ensureHttps = (url: string) => {
// if (!/^https?:\/\//i.test(url)) {
// return `https://${url}`;
Expand Down Expand Up @@ -171,7 +181,7 @@ export const MyProfile: NextPage = () => {
<>
<h2 className="text-2xl font-bold">{name || "Guest user"}</h2>
<div className="text-base-content">
<Address address={connectedAddress} />
<Address address={address} />
</div>
<p className={`text-base-content ${bio ? "" : "text-red-600"}`}>{bio || "no biography available"}</p>
{website && (
Expand All @@ -196,24 +206,26 @@ export const MyProfile: NextPage = () => {
<></>
)}
{/* Edit/Cancel Button */}
{isEditing ? (
<button className="absolute top-4 right-4 btn btn-secondary btn-sm" onClick={() => setIsEditing(false)}>
X Cancel
</button>
) : (
<button className="absolute top-4 right-4 btn btn-primary btn-sm" onClick={() => setIsEditing(true)}>
<PencilIcon className="h-5 w-5" />
Edit
</button>
)}
{isEditing ? (
<div className="mt-2 flex items-center gap-2">
<button className="cool-button" onClick={handleEditProfile}>
Save changes
</button>
</div>
) : (
""
{address === connectedAddress && (
<>
{isEditing ? (
<button className="absolute top-4 right-4 btn btn-secondary btn-sm" onClick={() => setIsEditing(false)}>
X Cancel
</button>
) : (
<button className="absolute top-4 right-4 btn btn-primary btn-sm" onClick={() => setIsEditing(true)}>
<PencilIcon className="h-5 w-5" />
Edit
</button>
)}
{isEditing && (
<div className="mt-2 flex items-center gap-2">
<button className="cool-button" onClick={handleEditProfile}>
Save changes
</button>
</div>
)}
</>
)}
</div>

Expand All @@ -238,3 +250,5 @@ export const MyProfile: NextPage = () => {
</div>
);
};

export default ProfilePage;
2 changes: 1 addition & 1 deletion packages/nextjs/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export const Header = () => {
{isMenuOpen && isConnected && (
<div className="absolute flex flex-col items-center justify-center right-0 top-10 mt-2 w-48 bg-base-300 shadow-lg rounded-lg">
<div className="my-2 flex flex-row items-center justify-center gap-2">
<Link href="/myProfile" passHref>
<Link href={`/profile/${connectedAddress}`} passHref>
<span className="btn btn-primary bg-base-200 border-0" onClick={handleMenuClose}>
My Profile
</span>
Expand Down

0 comments on commit 5963c89

Please sign in to comment.