From f638a13093bca1439894d588579a26ddfbab208f Mon Sep 17 00:00:00 2001 From: Cyrus Cowley Date: Fri, 29 Apr 2022 21:33:08 -0700 Subject: [PATCH] adding rental details popup and rendering more nfts --- .../src/client/components/ExploreRentals.tsx | 74 ++++-------------- .../src/client/components/ListingPanel.tsx | 12 ++- frontend/src/client/components/Popup.tsx | 26 +++++++ .../src/client/components/RentDetails.tsx | 32 ++++++++ frontend/src/client/lib/fetchNft.ts | 59 ++++++++++++++ frontend/src/client/pages/List.tsx | 76 +++++++++++++++---- 6 files changed, 200 insertions(+), 79 deletions(-) create mode 100644 frontend/src/client/components/Popup.tsx create mode 100644 frontend/src/client/components/RentDetails.tsx create mode 100644 frontend/src/client/lib/fetchNft.ts diff --git a/frontend/src/client/components/ExploreRentals.tsx b/frontend/src/client/components/ExploreRentals.tsx index e5f685c..b183658 100644 --- a/frontend/src/client/components/ExploreRentals.tsx +++ b/frontend/src/client/components/ExploreRentals.tsx @@ -1,10 +1,12 @@ import React, { useState, useEffect } from "react"; -import { NftWithMetadata, Nft, AvaliabilityStatus, Attribute } from "../../../types/nftTypes.js"; +import { NftWithMetadata, Nft } from "../../../types/nftTypes.js"; import { ListingPanel } from "./ListingPanel"; +import { Popup } from "./Popup"; +import { RentDetails } from "./RentDetails"; import { useMoralisQuery } from "react-moralis"; import DatePicker from "react-datepicker"; import "react-datepicker/dist/react-datepicker.css"; -import { getNFTs, getNFTMetadata } from "../lib/web3"; +import { mergeNftsWithMetadata } from "../lib/fetchNft"; export const ExploreRentals = () => { const [dateFilterVisible, setDateFilterVisible] = useState(false); @@ -13,64 +15,11 @@ export const ExploreRentals = () => { const [costFilterVisible, setCostFilterVisible] = useState(false); const [startCost, setStartCost] = useState(null); const [endCost, setEndCost] = useState(null); - const { data: rawNftListings, error, isLoading } = useMoralisQuery("Listing", query => query.limit(8), [], {}); + const { data: rawNftListings, error, isLoading } = useMoralisQuery("Listing", query => query.limit(8), [], {}); const [nfts, setNfts] = useState([]); - const getMetadataForAllNfts = async (nfts: Nft[]) => { - const awaitables = []; - for (const nft of nfts) { - awaitables.push(getNFTMetadata(nft.specification.collection, nft.specification.id)); - } - return Promise.all(awaitables); - }; - - const calculateAvaliabiltyStatus = (nft: Nft) => { - const now = new Date(); - if (nft.listing.datesForRent.some(date => date.startDate <= now && date.endDate >= now)) { - return AvaliabilityStatus.Avaliabile; - } else { - return AvaliabilityStatus.Unavaliabile; - } - }; - - const mapIpfsToUrl = (ipfs: string) => { - return `https://ipfs.io/ipfs/${ipfs.slice(7)}`; - }; - - const mergeNftsWithMetadata = async (nfts: Nft[]) => { - let nftsWithMetadata = []; - const metadatas = await getMetadataForAllNfts(nfts); - for (let i = 0; i < nfts.length; i++) { - const metadata = metadatas[i].metadata!; - - let attributes: Attribute[] = []; - if (metadata.attributes) { - attributes = metadata.attributes.map(attribute => { - return { - traitType: attribute.trait_type, - value: attribute.value, - }; - }); - } - - let image = metadata.image!; - - if (image.startsWith("ipfs://")) { - image = mapIpfsToUrl(image); - } - - const nftWithMetadata = { - nft: nfts[i], - name: metadata.name!, - image, - attributes, - avaliability: { status: calculateAvaliabiltyStatus(nfts[i]) }, - }; - nftsWithMetadata.push(nftWithMetadata); - } - setNfts(nftsWithMetadata); - }; + const [selectedNft, setSelectedNft] = useState(null); useEffect(() => { const nftListings: Nft[] = rawNftListings.map(nft => { @@ -79,7 +28,7 @@ export const ExploreRentals = () => { specification: nft.attributes.nftSpecification, }; }); - mergeNftsWithMetadata(nftListings); + mergeNftsWithMetadata(nftListings).then(nftsWithMetadata => setNfts(nftsWithMetadata)); }, [rawNftListings]); if (isLoading) { @@ -197,6 +146,11 @@ export const ExploreRentals = () => { return ( <> + {selectedNft && ( + setSelectedNft(null)}> + + + )}
{renderDateFilter()}
@@ -204,7 +158,9 @@ export const ExploreRentals = () => {
{filteredList.map((nft, index) => ( - +
setSelectedNft(nft)} key={index}> + +
))}
diff --git a/frontend/src/client/components/ListingPanel.tsx b/frontend/src/client/components/ListingPanel.tsx index 7cce071..9393fd2 100644 --- a/frontend/src/client/components/ListingPanel.tsx +++ b/frontend/src/client/components/ListingPanel.tsx @@ -1,7 +1,7 @@ import React from "react"; import { NftWithMetadata, Avaliability, AvaliabilityStatus } from "../../../types/nftTypes.js"; -export const ListingPanel = ({ nft }: { nft: NftWithMetadata }) => { +export const ListingPanel = ({ nft, pureNft = false }: { nft: NftWithMetadata; pureNft?: boolean }) => { const renderAvaliability = (availability: Avaliability) => { let statusText; switch (availability.status) { @@ -41,7 +41,7 @@ export const ListingPanel = ({ nft }: { nft: NftWithMetadata }) => {

{nft.name}

-

ETH({nft.nft.listing.pricePerDay}) / day

+ {!pureNft &&

ETH({nft.nft.listing.pricePerDay}) / day

}
{nft.attributes.map(attribute => (
{ {attribute.traitType}: {attribute.value}
))} -

{nft.nft.listing.description}

-
{renderAvaliability(nft.avaliability)}
+ {!pureNft && ( + <> +

{nft.nft.listing.description}

+
{renderAvaliability(nft.avaliability)}
+ + )} ); diff --git a/frontend/src/client/components/Popup.tsx b/frontend/src/client/components/Popup.tsx new file mode 100644 index 0000000..d2d5c09 --- /dev/null +++ b/frontend/src/client/components/Popup.tsx @@ -0,0 +1,26 @@ +import React, { ReactNode } from "react"; + +export const Popup = ({ closeHandler, children }: { closeHandler: () => void; children: ReactNode }) => { + return ( +
{ + if (e.target === e.currentTarget) { + closeHandler(); + } + }} + > +
+
+ ✖ +
+ {children} +
+
+ ); +}; diff --git a/frontend/src/client/components/RentDetails.tsx b/frontend/src/client/components/RentDetails.tsx new file mode 100644 index 0000000..01e1160 --- /dev/null +++ b/frontend/src/client/components/RentDetails.tsx @@ -0,0 +1,32 @@ +import React, { useState } from "react"; +import { NftWithMetadata } from "../../../types/nftTypes.js"; +import DatePicker from "react-datepicker"; + +export const RentDetails = ({ nft }: { nft: NftWithMetadata }) => { + const [dateRange, setDateRange] = useState<[Date, Date]>([new Date(), new Date()]); + const [startDate, endDate] = dateRange; + + return ( +
+

{nft.name}

+

{nft.nft.listing.description}

+

Pick your date:

+ { + setDateRange(update); + }} + minDate={new Date()} + showDisabledMonthNavigation + inline + /> +

Price per day: {nft.nft.listing.pricePerDay} ETH

+

Collateral: {nft.nft.listing.collateral} ETH

+ +
+ ); +}; diff --git a/frontend/src/client/lib/fetchNft.ts b/frontend/src/client/lib/fetchNft.ts new file mode 100644 index 0000000..47816bb --- /dev/null +++ b/frontend/src/client/lib/fetchNft.ts @@ -0,0 +1,59 @@ + +import { getNFTMetadata } from "./web3"; +import { NftWithMetadata, Nft, AvaliabilityStatus, Attribute } from "../../../types/nftTypes.js"; + + +const getMetadataForAllNfts = async (nfts: Nft[]) => { + const awaitables = []; + for (const nft of nfts) { + awaitables.push(getNFTMetadata(nft.specification.collection, nft.specification.id)); + } + return Promise.all(awaitables); +}; + +const calculateAvaliabiltyStatus = (nft: Nft) => { + const now = new Date(); + if (nft.listing.datesForRent.some(date => date.startDate <= now && date.endDate >= now)) { + return AvaliabilityStatus.Avaliabile; + } else { + return AvaliabilityStatus.Unavaliabile; + } +}; + +const mapIpfsToUrl = (ipfs: string) => { + return `https://ipfs.io/ipfs/${ipfs.slice(7)}`; +}; + +export const mergeNftsWithMetadata = async (nfts: Nft[]) => { + let nftsWithMetadata = []; + const metadatas = await getMetadataForAllNfts(nfts); + for (let i = 0; i < nfts.length; i++) { + const metadata = metadatas[i].metadata!; + + let attributes: Attribute[] = []; + if (metadata.attributes) { + attributes = metadata.attributes.map(attribute => { + return { + traitType: attribute.trait_type, + value: attribute.value, + }; + }); + } + + let image = metadata.image!; + + if (image.startsWith("ipfs://")) { + image = mapIpfsToUrl(image); + } + + const nftWithMetadata: NftWithMetadata = { + nft: nfts[i], + name: metadata.name!, + image, + attributes, + avaliability: { status: calculateAvaliabiltyStatus(nfts[i]) }, + }; + nftsWithMetadata.push(nftWithMetadata); + } + return nftsWithMetadata; +}; \ No newline at end of file diff --git a/frontend/src/client/pages/List.tsx b/frontend/src/client/pages/List.tsx index 9bf7061..da4c963 100644 --- a/frontend/src/client/pages/List.tsx +++ b/frontend/src/client/pages/List.tsx @@ -1,13 +1,16 @@ import { useFormik } from "formik"; -import React from "react"; +import React, { useState } from "react"; import { Footer } from "../components/Footer"; import { Navbar } from "../components/Navbar"; import { DayPicker } from "react-day-picker"; -import { Nft } from "../../../types/nftTypes.js"; +import { ListingPanel } from "../components/ListingPanel"; +import { mergeNftsWithMetadata } from "../lib/fetchNft"; import { useMoralis } from "react-moralis"; +import { NftWithMetadata, Nft } from "../../../types/nftTypes.js"; + import "react-day-picker/dist/style.css"; -const buttonStyle = "w-48 bg-indigo-800 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded-md"; +const buttonBaseStyle = "w-48 bg-indigo-800 text-white font-bold py-2 px-4 rounded-md"; const inputStyle = "border-2 flex-grow border-slate-500 p-2 rounded-md"; const labelStyle = "font-bold w-40"; const FormSection: React.FC<{ center?: boolean }> = ({ center, children }) => { @@ -18,6 +21,8 @@ const Divider = () =>
; const List = () => { const { Moralis } = useMoralis(); + const [nft, setNft] = useState(null); + const [validNft, setValidNft] = useState(false); // A complex subclass of Moralis.Object const Listing = Moralis.Object.extend( @@ -39,7 +44,7 @@ const List = () => { }); }; - const formik = useFormik({ + const { handleSubmit, handleChange, values, touched } = useFormik({ initialValues: { collection: "", id: "", @@ -63,10 +68,47 @@ const List = () => { }, }); + const handleBlur = async () => { + if (values.collection.trim() !== "" && values.id.trim() !== "") { + try { + const NftsWithMetadata = await mergeNftsWithMetadata([ + { + listing: { + description: values.description, + datesForRent: [], + pricePerDay: values.pricePerDay, + collateral: values.collateral, + }, + specification: { + collection: values.collection, + id: values.id, + }, + }, + ]); + setNft(NftsWithMetadata[0]); + setValidNft(true); + } catch (e) { + console.log("BAD NFT"); + setValidNft(false); + } + } + }; + + let buttonStyle = buttonBaseStyle; + + if (validNft) { + buttonStyle += " hover:bg-indigo-700"; + } + return (
-
+ {nft && ( +
+ +
+ )} +

List a Rental

{/*
@@ -87,15 +129,17 @@ const List = () => { @@ -106,8 +150,8 @@ const List = () => { @@ -126,8 +170,8 @@ const List = () => { type="number" step="any" min="0" - onChange={formik.handleChange} - value={formik.values.pricePerDay} + onChange={handleChange} + value={values.pricePerDay} className={inputStyle} > { type="number" step="any" min="0" - onChange={formik.handleChange} - value={formik.values.collateral} + onChange={handleChange} + value={values.collateral} className={inputStyle} > @@ -149,7 +193,7 @@ const List = () => { */} -