Skip to content

Commit

Permalink
completed
Browse files Browse the repository at this point in the history
  • Loading branch information
malerba118 committed Feb 16, 2022
1 parent 1086d47 commit fdaf836
Show file tree
Hide file tree
Showing 10 changed files with 325 additions and 1 deletion.
36 changes: 36 additions & 0 deletions components/AuthButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as React from "react";
import { Button, ButtonProps } from "@chakra-ui/react";
import { useAccount, useConnect } from "wagmi";
import toast from "react-hot-toast";

interface AuthButtonProps extends ButtonProps {}

const AuthButton: React.FunctionComponent<AuthButtonProps> = (props) => {
const [connectQuery, connect] = useConnect();
const [accountQuery] = useAccount();

React.useEffect(() => {
if (connectQuery.error?.name === "ConnectorNotFoundError") {
toast.error("Metamask extension required to sign in");
}
}, [connectQuery.error]);

// If not authenticated, require sign-in
if (!accountQuery.data?.address) {
return (
<Button
{...props}
onClick={() => {
connect(connectQuery.data.connectors[0]);
}}
>
Sign In
</Button>
);
}

// If authenticated, show button as usual
return <Button {...props}>{props.children}</Button>;
};

export default AuthButton;
31 changes: 31 additions & 0 deletions components/Comment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as React from "react";
import { Text, Heading, HStack, Stack } from "@chakra-ui/react";
import TimeAgo from "react-timeago";
import Avatar from "@davatar/react";
import Username from "./Username";
import { Comment } from "../hooks/useCommentsContract";

interface CommentProps {
comment: Comment;
}

const Comment: React.FunctionComponent<CommentProps> = ({ comment }) => {
return (
<HStack spacing={3} alignItems="start">
<Avatar size={48} address={comment.creator_address} />
<Stack spacing={1} flex={1} bg="whiteAlpha.100" rounded="2xl" p={3}>
<Heading color="whiteAlpha.900" fontSize="lg">
<Username address={comment.creator_address} />
</Heading>
<Text color="whiteAlpha.800" fontSize="lg">
{comment.message}
</Text>
<Text color="whiteAlpha.500" fontSize="md">
<TimeAgo date={comment.created_at.toNumber() * 1000} />
</Text>
</Stack>
</HStack>
);
};

export default Comment;
64 changes: 64 additions & 0 deletions components/CommentEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { HStack, Stack, Textarea } from "@chakra-ui/react";
import * as React from "react";
import Avatar from "@davatar/react";
import AuthButton from "./AuthButton";
import { useAccount } from "wagmi";
import useAddComment from "../hooks/useAddComment";

interface CommentEditorProps {
topic: string;
}

const CommentEditor: React.FunctionComponent<CommentEditorProps> = ({
topic,
}) => {
const [message, setMessage] = React.useState("");
const mutation = useAddComment();
const [accountQuery] = useAccount();

return (
<Stack spacing={3}>
<HStack spacing={3} alignItems="start">
<Avatar
size={48}
address={
accountQuery.data?.address ||
"0x0000000000000000000000000000000000000000"
}
/>
<Textarea
value={message}
onChange={(e) => {
setMessage(e.target.value);
}}
placeholder="Write a message.."
p={3}
flex={1}
bg="whiteAlpha.100"
rounded="2xl"
fontSize="lg"
/>
</HStack>
<AuthButton
size="sm"
colorScheme="pink"
alignSelf="flex-end"
onClick={() => {
mutation
.mutateAsync({
message,
topic,
})
.then(() => {
setMessage("");
});
}}
isLoading={mutation.isLoading}
>
Submit
</AuthButton>
</Stack>
);
};

export default CommentEditor;
34 changes: 34 additions & 0 deletions components/Comments.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import * as React from "react";
import { Box, Spinner, Stack, Center } from "@chakra-ui/react";
import Comment from "./Comment";
import CommentEditor from "./CommentEditor";
import useComments from "../hooks/useComments";
import useEvents from "../hooks/useEvents";

interface CommentsProps {
topic: string;
}

const Comments: React.FunctionComponent<CommentsProps> = ({ topic }) => {
const query = useComments({ topic });

useEvents({ topic });

return (
<Box>
{query.isLoading && (
<Center p={8}>
<Spinner />
</Center>
)}
<Stack spacing={4}>
{query.data?.map((comment) => (
<Comment key={comment.id} comment={comment} />
))}
{query.isFetched && <CommentEditor topic={topic} />}
</Stack>
</Box>
);
};

export default Comments;
28 changes: 28 additions & 0 deletions components/Username.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as React from "react";
import { Text, TextProps } from "@chakra-ui/react";
import { useEnsLookup } from "wagmi";
import truncateMiddle from "truncate-middle";

interface UsernameProps extends TextProps {
address: string;
}

const Username: React.FunctionComponent<UsernameProps> = ({
address,
...otherProps
}) => {
const [query] = useEnsLookup({ address });

// Show ens name if exists, but show truncated address as fallback
return (
<Text
display="inline"
textTransform={query.data ? "none" : "uppercase"}
{...otherProps}
>
{query.data || truncateMiddle(address || "", 5, 4, "...")}
</Text>
);
};

export default Username;
17 changes: 17 additions & 0 deletions hooks/useAddComment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useMutation } from "react-query";
import useCommentsContract from "./useCommentsContract";

interface UseAddCommentPayload {
topic: string;
message: string;
}

const useAddComment = () => {
const contract = useCommentsContract();

return useMutation(async ({ topic, message }: UseAddCommentPayload) => {
await contract.addComment(topic, message);
});
};

export default useAddComment;
15 changes: 15 additions & 0 deletions hooks/useComments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useQuery } from "react-query";
import useCommentsContract from "./useCommentsContract";

interface UseCommentsQuery {
topic: string;
}

const useComments = ({ topic }: UseCommentsQuery) => {
const contract = useCommentsContract();
return useQuery(["comments", { topic, chainId: contract.chainId }], () =>
contract.getComments(topic)
);
};

export default useComments;
62 changes: 62 additions & 0 deletions hooks/useCommentsContract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import * as wagmi from "wagmi";
import { useProvider, useSigner } from "wagmi";
import type { BigNumber } from "ethers";
// Import our contract ABI
import CommentsContract from "../artifacts/contracts/Comments.sol/Comments.json";

export interface Comment {
id: string;
topic: string;
message: string;
creator_address: string;
created_at: BigNumber;
}

export enum EventType {
CommentAdded = "CommentAdded",
}

const useCommentsContract = () => {
// An ethers.Signer instance associated with the signed-in wallet.
// https://docs.ethers.io/v5/api/signer/
const [signer] = useSigner();
// An ethers.Provider instance associated with the connected blockchain network.
// (eg localhost, polygon-mumbai, ethereum-mainnet)
// https://docs.ethers.io/v5/api/providers/
const provider = useProvider();

// This returns a new ethers.Contract ready to interact with our comments API.
// We need to pass in the address of our deployed contract as well as its abi.
// We also pass in the signer if there is a signed in wallet, or if there's
// no signed in wallet then we'll pass in the connected provider.
const contract = wagmi.useContract({
addressOrName: "0x5FbDB2315678afecb367f032d93F642f64180aa3",
contractInterface: CommentsContract.abi,
signerOrProvider: signer.data || provider,
});

// Wrapper to add types to our getComments function.
const getComments = async (topic: string): Promise<Comment[]> => {
return contract.getComments(topic).then((comments) => {
// Each comment is represented as array by default so we convert to object
return comments.map((c) => ({ ...c }));
});
};

// Wrapper to add types to our addComment function.
const addComment = async (topic: string, message: string): Promise<void> => {
// Create a new transaction
const tx = await contract.addComment(topic, message);
// Wait for transaction to be mined
await tx.wait();
};

return {
contract,
chainId: contract.provider.network?.chainId,
getComments,
addComment,
};
};

export default useCommentsContract;
36 changes: 36 additions & 0 deletions hooks/useEvents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useEffect } from "react";
import { useQueryClient } from "react-query";
import useCommentsContract, { EventType } from "./useCommentsContract";

interface UseEventsQuery {
topic: string;
}

// Listen to events and refresh data
const useEvents = ({ topic }: UseEventsQuery) => {
const queryClient = useQueryClient();
const commentsContract = useCommentsContract();

useEffect(() => {
const handler = (comment) => {
if (comment.topic !== topic) {
return;
}
// Invalidates the query whose query key matches the passed array.
// This will cause the useComments hook to re-render the Comments
// component with fresh data.
queryClient.invalidateQueries([
"comments",
{ topic: comment.topic, chainId: commentsContract.chainId },
]);
};

commentsContract.contract.on(EventType.CommentAdded, handler);

return () => {
commentsContract.contract.off(EventType.CommentAdded, handler);
};
}, [queryClient, commentsContract.chainId, topic]);
};

export default useEvents;
3 changes: 2 additions & 1 deletion pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Toaster, toast } from "react-hot-toast";
import theme from "../theme";
import { Provider as WagmiProvider } from "wagmi";
import { providers } from "ethers";
import Comments from "../components/Comments";

// Provide a fallback network while chainId is not yet defined
const provider = ({ chainId, connector }) => {
Expand Down Expand Up @@ -37,7 +38,7 @@ const App: NextPage = () => {
<ChakraProvider theme={theme}>
<QueryClientProvider client={queryClient}>
<Box p={8} maxW="600px" minW="320px" m="0 auto">
<Heading>Oops, no comments yet!</Heading>
<Comments topic="my-blog-post" />
<Toaster position="bottom-right" />
</Box>
</QueryClientProvider>
Expand Down

0 comments on commit fdaf836

Please sign in to comment.