diff --git a/src/components/CollectionBanner.tsx b/src/components/CollectionBanner.tsx index 8921ef4..a02a353 100644 --- a/src/components/CollectionBanner.tsx +++ b/src/components/CollectionBanner.tsx @@ -292,8 +292,9 @@ const CollectionBanner = (props: ICollectionBanner & PropsFromRedux) => { ({ + type: "NFT", nftRecord: item, - assetRecord: collectionDataTanstack?.nftAssets[item?.asset_address], + collectionRecord: collectionDataTanstack?.nftAssets[item?.asset_address], })) : [] } fullWidth={true} diff --git a/src/components/CollectionExplorerGallery.tsx b/src/components/CollectionExplorerGallery.tsx index 05d09bf..02b4342 100644 --- a/src/components/CollectionExplorerGallery.tsx +++ b/src/components/CollectionExplorerGallery.tsx @@ -13,13 +13,18 @@ import Button from '@mui/material/Button'; import ArrowNext from '@mui/icons-material/ArrowForwardIos'; import ArrowPrevious from '@mui/icons-material/ArrowBackIosNew'; -import { getResolvableIpfsLink } from '../utils'; +import { getResolvableIpfsLink, priceFormat } from '../utils'; import LinkWrapper from '../components/LinkWrapper'; import NFTLikeZoneContainer from '../containers/NFTLikeZoneContainer'; +import PropyKeysHomeListingLikeZoneContainer from '../containers/PropyKeysHomeListingLikeZoneContainer'; import GenericTitleContainer from '../containers/GenericTitleContainer'; +import BathroomIcon from '../assets/svg/bathroom-icon.svg'; +import BedroomIcon from '../assets/svg/bedroom-icon.svg'; +import LotSizeIcon from '../assets/svg/lot-size-icon.svg'; + import { PropsFromRedux } from '../containers/CollectionExplorerGalleryContainer' import { @@ -121,11 +126,37 @@ const useStyles = makeStyles((theme: Theme) => position: 'absolute', }, likeContainer: { - marginTop: theme.spacing(1), + marginTop: theme.spacing(2), }, descriptionSpacerMobile: { marginBottom: theme.spacing(2), - } + }, + priceZone: { + marginTop: theme.spacing(2), + }, + quickSpecsZone: { + marginTop: theme.spacing(2), + display: 'flex', + }, + quickSpecsZoneMobile: { + marginBottom: theme.spacing(2), + }, + quickSpecEntry: { + display: 'flex', + alignItems: 'center', + marginRight: theme.spacing(2) + }, + quickSpecEntryIcon: { + height: 30, + marginRight: theme.spacing(1), + }, + quickSpecEntryText: { + textTransform: 'none', + fontSize: '1rem', + }, + descriptionSpacerDesktop: { + marginBottom: theme.spacing(2), + }, }), ) @@ -196,53 +227,117 @@ const CollectionExplorerGallery = ({ config: { duration: 100 }, }) - // const infoTransitions = useTransition(getVisibleIndices(), { - // from: (index) => ({ - // opacity: 0, - // transform: `scale(${index === selectedEntryIndex ? 1 : 0.5})`, - // top: index === selectedEntryIndex ? `0px` : `0px` - // }), - // enter: (index) => ({ - // opacity: index === selectedEntryIndex ? 1 : 0, - // transform: `scale(${index === selectedEntryIndex ? 1 : 0.5})`, - // top: index === selectedEntryIndex ? `0px` : `0px`, - // }), - // update: (index) => ({ - // opacity: index === selectedEntryIndex ? 1 : 0, - // transform: `scale(${index === selectedEntryIndex ? 1 : 0.5})`, - // top: index === selectedEntryIndex ? `0px` : `0px`, - // }), - // leave: { opacity: 0 }, - // config: { duration: 200 }, - // }) + const { + type = false, + } = explorerEntries[selectedEntryIndex] ? explorerEntries[selectedEntryIndex] : {}; + + const listingRecord = (explorerEntries[selectedEntryIndex]?.type === "LISTING" && explorerEntries[selectedEntryIndex]?.listingRecord) ? explorerEntries[selectedEntryIndex]?.listingRecord : false; const { token_id = false, asset_address = false, network_name = false, metadata = false, - } = explorerEntries[selectedEntryIndex]?.nftRecord ? explorerEntries[selectedEntryIndex]?.nftRecord : {}; + } = (explorerEntries[selectedEntryIndex]?.type === "NFT" && explorerEntries[selectedEntryIndex]?.nftRecord) ? explorerEntries[selectedEntryIndex]?.nftRecord : {}; const { name = false, collection_name = false, slug = false, - } = explorerEntries[selectedEntryIndex]?.assetRecord ? explorerEntries[selectedEntryIndex]?.assetRecord : {}; + } = explorerEntries[selectedEntryIndex]?.collectionRecord ? explorerEntries[selectedEntryIndex]?.collectionRecord : {}; + + const renderPrimaryContentListing = () => { + if(listingRecord) { + let quickSpecs = []; + + if(listingRecord?.bathrooms) { + quickSpecs.push({ + icon: BathroomIcon, + value: `${listingRecord?.bathrooms} ba` + }) + } + + if(listingRecord?.bedrooms) { + quickSpecs.push({ + icon: BedroomIcon, + value: `${listingRecord?.bedrooms} bd` + }) + } + + if(listingRecord?.lot_size) { + quickSpecs.push({ + icon: LotSizeIcon, + value: `${listingRecord?.lot_size} ft²` + }) + } + + if(listingRecord) { + return ( + <> + + + { + `PropyKeys Home Listings` + } + + + + { + listingRecord && listingRecord?.full_address + } + + + { + listingRecord?.price && priceFormat(listingRecord?.price, 2, "$") + } + + {quickSpecs.length > 0 && +
+ {quickSpecs.map((entry) => +
+ {entry.value} + {entry.value} +
+ )} +
+ } + {listingRecord?.id && +
+ +
+ } + { + listingRecord.token_id && + listingRecord.asset_address && + listingRecord.network_name && +
+ + + +
+ } + + ) + } + } + } return ( -
-
-
+
+
+
{!isLoading && explorerEntries.length > 0 && transitions((style, index) => { if (typeof index !== 'number' || isNaN(index) || index < 0 || index >= explorerEntries.length) { return null; } return ( ); @@ -251,7 +346,7 @@ const CollectionExplorerGallery = ({
} { - (explorerEntries.length > 0) && + (explorerEntries.length > 1) &&
handlePrevious()} color="default" aria-label="previous"> @@ -262,49 +357,58 @@ const CollectionExplorerGallery = ({
}
-
- - - {!overrideTitle && - `${collection_name ? collection_name : name}` +
+ {explorerEntries[selectedEntryIndex]?.type === "NFT" && + <> + + + {!overrideTitle && + `${collection_name ? collection_name : name}` + } + {overrideTitle && overrideTitle} + + + + {metadata && metadata?.name ? metadata?.name : ""} + + { + token_id && + asset_address && + network_name && +
+ +
} - {overrideTitle && overrideTitle} - - - - {metadata && metadata?.name ? metadata?.name : ""} - - { - token_id && - asset_address && - network_name && -
- -
+ {metadata && metadata?.description && + <> + + {metadata?.description} + + } + { + token_id && + asset_address && + network_name && +
+ + + +
+ } + } - {metadata && metadata?.description && + {explorerEntries[selectedEntryIndex]?.type === "LISTING" && <> - - {metadata?.description} + {renderPrimaryContentListing()} } - { - token_id && - asset_address && - network_name && -
- - - -
- }
diff --git a/src/components/MultiCollectionGallery.tsx b/src/components/MultiCollectionGallery.tsx new file mode 100644 index 0000000..6b3ec19 --- /dev/null +++ b/src/components/MultiCollectionGallery.tsx @@ -0,0 +1,239 @@ +import React, { useState, useId } from 'react'; + +import { Theme } from '@mui/material/styles'; + +import makeStyles from '@mui/styles/makeStyles'; +import createStyles from '@mui/styles/createStyles'; + +import Typography from '@mui/material/Typography'; + +import Card from '@mui/material/Card'; + +import { + COLLECTIONS_PAGE_ENTRIES, + LISTING_COLLECTIONS_PAGE_ENTRIES, + PROPY_LIGHT_BLUE, +} from '../utils/constants'; + +import { useQuery } from '@tanstack/react-query'; + +import { + INFTRecord, + ICollectionRecord, + IRecentlyMintedResult, + ICollectionQueryFilter, + IExplorerGalleryEntry, + IPropyKeysHomeListingRecord, +} from '../interfaces'; + +import { + NFTService, + PropyKeysListingService, +} from '../services/api'; + +import { PropsFromRedux } from '../containers/MultiCollectionGalleryContainer'; + +import CollectionExplorerGalleryContainer from '../containers/CollectionExplorerGalleryContainer'; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + width: '100%', + display: 'flex', + flexDirection: 'column', + }, + collectionEntries: { + width: '100%', + overflowX: 'scroll', + whiteSpace: 'nowrap', + }, + collectionEntryOption: { + marginBottom: theme.spacing(2), + padding: theme.spacing(2), + display: 'inline-block', + }, + collectionEntryOptionRightMargin: { + marginRight: theme.spacing(2), + }, + selectedCollectionEntryOption: { + backgroundColor: `${PROPY_LIGHT_BLUE}`, + color: 'white', + }, + unselectedCollectionEntryOption: { + // border: `2px solid transparent`, + cursor: 'pointer', + }, + title: { + fontWeight: '500', + marginBottom: theme.spacing(2), + }, + }), +); + +const multiGalleryCollectionEntries = [...COLLECTIONS_PAGE_ENTRIES.filter((entry) => entry.showInMultiCollectionGallery), ...LISTING_COLLECTIONS_PAGE_ENTRIES.filter((entry) => entry.showInMultiCollectionGallery)]; + +interface IMultiCollectionGallery { + +} + +const MultiCollectionGallery = (props: PropsFromRedux & IMultiCollectionGallery) => { + + const classes = useStyles(); + + const uniqueId = useId(); + + const [selectedCollectionIndex, setSelectedCollectionIndex] = useState(0); + const [resetIndex, setResetIndex] = useState(0); + + const { + isConsideredMobile, + } = props; + + const perPage = 20; + + const { + address, + network, + overrideTitle, + optimisticTitle, + slug, + collectionType, + } = multiGalleryCollectionEntries[selectedCollectionIndex] + + const { + data: collectionDataTanstack, + isLoading: isLoadingCollectionDataTanstack, + } = useQuery({ + queryKey: ['multi-collection-gallery', address, network, slug, overrideTitle, collectionType], + queryFn: async () => { + let additionalFilters : ICollectionQueryFilter[] = []; + additionalFilters.push({filter_type: "sort_by", value: "most_liked"}); + let title = "No records found"; + if(collectionType === "NFT") { + let collectionResponse = await NFTService.getCollectionPaginated( + network, + address, + perPage, + 1, + additionalFilters, + ) + if(collectionResponse?.status && collectionResponse?.data) { + let renderResults : INFTRecord[] = []; + let assetResults : {[key: string]: ICollectionRecord} = {}; + let apiResponseData : IRecentlyMintedResult = collectionResponse.data; + if(collectionResponse?.status && apiResponseData?.data) { + for(let nftRecord of apiResponseData?.data) { + if(nftRecord?.asset?.address && !assetResults[nftRecord?.asset?.address]) { + assetResults[nftRecord.asset.address] = nftRecord.asset; + title = "Collection not found"; + if(nftRecord.asset.collection_name) { + title = nftRecord.asset.collection_name; + } else if (nftRecord.asset.name) { + title = nftRecord.asset.name; + } else if (nftRecord.asset.address) { + title = nftRecord.asset.address; + } + if(overrideTitle) { + title = overrideTitle; + } + } + renderResults = [...renderResults, nftRecord]; + } + } + return { + title, + collectionType, + nftRecords: renderResults, + collectionRecord: assetResults, + activeFilters: additionalFilters, + listingRecords: [], + } + } + } else if (collectionType === "LISTING") { + let renderResults : IPropyKeysHomeListingRecord[] = []; + let title = optimisticTitle ? optimisticTitle : "No records found"; + let collectionResponse = await PropyKeysListingService.getCollectionPaginated( + network, + slug, + perPage, + 1, + additionalFilters, + ) + if(collectionResponse?.status && collectionResponse?.data?.data) { + for(let listing of collectionResponse?.data?.data) { + renderResults.push(listing); + } + } + return { + title, + collectionType, + listingRecords: renderResults, + nftRecords: [], + collectionRecord: { + slug, + name: title, + collection_name: title, + }, + activeFilters: additionalFilters, + } + } + return { + title, + collectionType, + nftRecords: [], + listingRecords: [], + collectionRecord: {}, + activeFilters: [], + } + }, + gcTime: 5 * 60 * 1000, + staleTime: 60 * 1000, + }); + + let explorerEntries : IExplorerGalleryEntry[] = []; + if(collectionDataTanstack?.collectionType === "NFT") { + explorerEntries = collectionDataTanstack?.nftRecords ? collectionDataTanstack?.nftRecords.map((item, index) => ({ + type: "NFT", + nftRecord: item, + collectionRecord: collectionDataTanstack?.collectionRecord[item?.asset_address], + })) : [] + } else if (collectionDataTanstack?.collectionType === "LISTING") { + explorerEntries = collectionDataTanstack?.listingRecords ? collectionDataTanstack?.listingRecords.map((item, index) => ({ + type: "LISTING", + listingRecord: item, + collectionRecord: collectionDataTanstack?.collectionRecord, + })) : [] + } + + return ( +
+ + Top Rated Collection Items + +
+
+ {multiGalleryCollectionEntries.map((item, index) => { + return ( + {setSelectedCollectionIndex(index);setResetIndex(resetIndex + 1)}} + className={[classes.collectionEntryOption, index === selectedCollectionIndex ? classes.selectedCollectionEntryOption : classes.unselectedCollectionEntryOption, index < multiGalleryCollectionEntries.length - 1 ? classes.collectionEntryOptionRightMargin : ""].join(" ")} + key={`${uniqueId}-multi-collection-${item.address}-${index}`}> + {item.optimisticTitle} + + ) + })} +
+ +
+
+ ) +} + +export default MultiCollectionGallery; \ No newline at end of file diff --git a/src/components/PropyKeysHomeListingLikeZone.tsx b/src/components/PropyKeysHomeListingLikeZone.tsx index 2d1fac1..8360bf9 100644 --- a/src/components/PropyKeysHomeListingLikeZone.tsx +++ b/src/components/PropyKeysHomeListingLikeZone.tsx @@ -11,6 +11,7 @@ import createStyles from '@mui/styles/createStyles'; import IconButton from '@mui/material/IconButton'; import Tooltip from '@mui/material/Tooltip'; +import CircularProgress from '@mui/material/CircularProgress'; import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder'; import FavoriteIcon from '@mui/icons-material/Favorite'; @@ -61,6 +62,7 @@ interface IPropyKeysHomeListingLikeZone { const PropyKeysHomeListingLikeZone = (props: PropsFromRedux & IPropyKeysHomeListingLikeZone) => { + const [loading, setLoading] = useState(true); const [likeCount, setLikeCount] = useState(0); const [isLiked, setIsLiked] = useState(false); const [reloadIndex, setReloadIndex] = useState(0); @@ -87,6 +89,9 @@ const PropyKeysHomeListingLikeZone = (props: PropsFromRedux & IPropyKeysHomeList useEffect(() => { let isMounted = true; const getLikeStatus = async () => { + if(isMounted) { + setLoading(true); + } if(address) { let [likeStatusResponse, likeCountResponse] = await Promise.all([ PropyKeysListingService.getLikedByStatus(propyKeysHomeListingId, address), @@ -114,6 +119,9 @@ const PropyKeysHomeListingLikeZone = (props: PropsFromRedux & IPropyKeysHomeList } setIsLiked(false); } + if(isMounted) { + setLoading(false); + } } getLikeStatus(); return () => { @@ -223,10 +231,15 @@ const PropyKeysHomeListingLikeZone = (props: PropsFromRedux & IPropyKeysHomeList } - - {likeCount} - {!compact && <>{(likeCount && (likeCount === 1)) ? ' Like' : ' Likes'}} - + {!loading && + + {likeCount} + {!compact && <>{(likeCount && (likeCount === 1)) ? ' Like' : ' Likes'}} + + } + {loading && + + }
) } diff --git a/src/containers/MultiCollectionGalleryContainer.tsx b/src/containers/MultiCollectionGalleryContainer.tsx new file mode 100644 index 0000000..316df86 --- /dev/null +++ b/src/containers/MultiCollectionGalleryContainer.tsx @@ -0,0 +1,19 @@ +import { connect, ConnectedProps } from 'react-redux'; + +import MultiCollectionGallery from '../components/MultiCollectionGallery'; + +interface RootState { + isConsideredMobile: boolean + isConsideredMedium: boolean +} + +const mapStateToProps = (state: RootState) => ({ + isConsideredMobile: state.isConsideredMobile, + isConsideredMedium: state.isConsideredMedium, +}) + +const connector = connect(mapStateToProps, {}) + +export type PropsFromRedux = ConnectedProps + +export default connector(MultiCollectionGallery) \ No newline at end of file diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 39f4cd7..2c416b3 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -143,11 +143,30 @@ export interface INFTRecord { transfer_events_erc721?: ITransferEventERC721Record[]; } -export interface IExplorerGalleryEntry { - nftRecord: INFTRecord, - assetRecord: IAssetRecord, +export interface INFTAsset { + [key: string]: IAssetRecord } +export interface ICollectionRecord { + name: string; + collection_name: string; + slug: string; +} + +type NFTExplorerGalleryEntry = { + type: 'NFT'; + nftRecord: INFTRecord; + collectionRecord: ICollectionRecord; +} + +type ListingExplorerGalleryEntry = { + type: 'LISTING'; + listingRecord: IPropyKeysHomeListingRecord; + collectionRecord: ICollectionRecord; +} + +export type IExplorerGalleryEntry = NFTExplorerGalleryEntry | ListingExplorerGalleryEntry; + export interface ICoordinate { longitude: number latitude: number diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index c295bda..80788f5 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -12,6 +12,7 @@ import { } from '../utils/constants'; import GenericPageContainer from '../containers/GenericPageContainer'; +import MultiCollectionGalleryContainer from '../containers/MultiCollectionGalleryContainer'; import AccountTokensBannerContainer from '../containers/AccountTokensBannerContainer'; import RecentlyMintedTokensBannerContainer from '../containers/RecentlyMintedTokensBannerContainer'; import CollectionBannerContainer from '../containers/CollectionBannerContainer'; @@ -59,6 +60,9 @@ const HomePage = () => {
+
+ +
{address &&
diff --git a/src/utils/constants.ts b/src/utils/constants.ts index bb4166e..a1d4eba 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -48,81 +48,105 @@ export const VALID_PROPYKEYS_COLLECTION_NAMES_OR_ADDRESSES = ["propykeys", "0xa2 export const VALID_HOME_LISTING_COLLECTION_NAMES_OR_ADDRESSES = ["propykeys-base-sepolia", "propykeys", "0xa239b9b3E00637F29f6c7C416ac95127290b950E", "0x45C395851c9BfBd3b7313B35E6Ee460D461d585c"]; -const COLLECTIONS_ENTRIES_DEV = [ +const COLLECTIONS_ENTRIES_DEV : ICollectionEntry[] = [ { network: "base-sepolia", address: "0x07922CDe9e58fb590ffB59BB8777cF4b737fE2a3", - slug: "propykeys-staking-sepolia" + slug: "propykeys-staking-sepolia", + collectionType: "NFT", }, { network: "base-sepolia", address: "0x4ebCEb82B5940E10c301A33261Af13222A38d974", slug: "propykeys-og-staking-sepolia", + collectionType: "NFT", }, { network: "base-sepolia", address: "0x45C395851c9BfBd3b7313B35E6Ee460D461d585c", slug: "propy-home-nft-dev-base-testnet", + collectionType: "NFT", }, { network: "goerli", address: "0x8fbFe4036F13e8E42E51235C9adA6efD2ACF6A95", slug: "propy-deed-certificates-stage-testnet", + collectionType: "NFT", }, { network: "goerli", address: "0x73C3a1437B0307732Eb086cb2032552eBea15444", slug: "propy-deed-certificates-dev-testnet", + collectionType: "NFT", }, ] -const COLLECTIONS_ENTRIES_PROD = [ +const COLLECTIONS_ENTRIES_PROD : ICollectionEntry[] = [ { network: "base", address: "0xa239b9b3E00637F29f6c7C416ac95127290b950E", slug: "propykeys?landmark=true&sort_by=most_liked", overrideTitle: "PropyKeys AI Landmarks", + optimisticTitle: "PropyKeys AI Landmarks", filterShims: ["landmark"], - showHeroGallery: true, - sortBy: "most_liked" + sortBy: "most_liked", + showInMultiCollectionGallery: true, + collectionType: "NFT", }, { network: "base", address: "0xa239b9b3E00637F29f6c7C416ac95127290b950E", + optimisticTitle: "PropyKeys Addresses", slug: "propykeys", + collectionType: "NFT", }, { network: "ethereum", address: "0x2dbC375B35c5A2B6E36A386c8006168b686b70D3", + optimisticTitle: "Real World Assets", slug: "propy-real-world-assets", + showInMultiCollectionGallery: true, + collectionType: "NFT", }, { network: "arbitrum", address: "0x567c407D054A644DBBBf2d3a6643776473f82d7a", + optimisticTitle: "Deed Certificates", slug: "propy-deed-certificates", + showInMultiCollectionGallery: true, + collectionType: "NFT", }, { network: "ethereum", address: "0xB5c4910335D373eb26FeBb30B8f1d7416179A4EC", + optimisticTitle: "MetaAgents", slug: "meta-agents", + showInMultiCollectionGallery: true, + collectionType: "NFT", }, ] -const LISTING_COLLECTIONS_ENTRIES_PROD = [ +const LISTING_COLLECTIONS_ENTRIES_PROD : ICollectionEntry[] = [ { network: "base", slug: "propykeys", overrideTitle: "PropyKeys Home Listings", address: "0xa239b9b3E00637F29f6c7C416ac95127290b950E", + showInMultiCollectionGallery: true, + optimisticTitle: "PropyKeys Home Listings", + collectionType: "LISTING", }, ] -const LISTING_COLLECTIONS_ENTRIES_DEV = [ +const LISTING_COLLECTIONS_ENTRIES_DEV : ICollectionEntry[] = [ { network: "base-sepolia", slug: "propykeys-base-sepolia", overrideTitle: "PropyKeys Home Listings (Base Sepolia)", address: "0x45C395851c9BfBd3b7313B35E6Ee460D461d585c", + showInMultiCollectionGallery: true, + optimisticTitle: "PropyKeys Home Listings", + collectionType: "LISTING", }, ] @@ -259,9 +283,12 @@ interface ICollectionEntry { address: string; slug: string; overrideTitle?: string; + optimisticTitle?: string; filterShims?: string[]; showHeroGallery?: boolean; + collectionType: "NFT" | "LISTING", sortBy?: "most_liked"; + showInMultiCollectionGallery?: boolean; } export const COLLECTIONS_PAGE_ENTRIES : ICollectionEntry[] = process?.env?.REACT_APP_ENV === 'prod' ? COLLECTIONS_ENTRIES_PROD : [...COLLECTIONS_ENTRIES_DEV, ...COLLECTIONS_ENTRIES_PROD]; @@ -274,4 +301,4 @@ export const LISTING_COLLECTIONS_PAGE_ENTRIES : ICollectionEntry[] = process?.en export const STAKING_ORIGIN_COUNTRY_BLACKLIST = ["US", "ZA", "CA"]; export const HOME_LISTING_CARD_HEIGHT = 327; -export const HOME_LISTING_CARD_MEDIA_HEIGHT = 220; \ No newline at end of file +export const HOME_LISTING_CARD_MEDIA_HEIGHT = 232; \ No newline at end of file