updated frontend to display nft gallery from 3box/opensea (#222)

* updated backend to read 3box collectiblesFavorite

* removed unneccesary '?'

* updated frontend to display nft gallery from 3box/opensea

* prettier format

* using theme colors/sizes

* fixed nft price string
This commit is contained in:
dan13ram
2020-12-16 23:46:31 +05:30
committed by GitHub
parent 566be91206
commit aa0a532d4a
13 changed files with 2145 additions and 171 deletions

View File

@@ -10,7 +10,6 @@ If you're new to the MetaGame codebase, check out the following guides to learn
## Development
### Bootstrap
Create your local .env file

View File

@@ -58,7 +58,7 @@ CONTAINER ID IMAGE COMMAND CREATED
38e3140ab632 postgres:12 "docker-entrypoint.s…" 51 minutes ago Up 2 minutes 0.0.0.0:5432->5432/tcp the-game_database_1
```
You can also read the logs of the services by running `docker-compose logs -f $SERVICE` (replace $SERVICE by `backend` or `hasura`)
You can also read the logs of the services by running `docker-compose logs -f $SERVICE` (replace \$SERVICE by `backend` or `hasura`)
```bash
$ docker-compose logs -f backend
@@ -83,11 +83,13 @@ Which populates it with testing data.
If you want to run the NodeJS backend service out of docker to be able to debug with your IDE:
**Add environment variable** to tell hasura where to find the backend (may only work on MacOS)
```shell script
echo 'BACKEND_HOST=host.docker.internal:4000' >> .env
```
**Start the server**
```shell script
yarn backend:dev
```
@@ -117,15 +119,15 @@ When creating a table, keep in mind a few things.
1. You should have an `id` for all table relationships and all datatypes should use snake_case.
- **Example 1:** Table Name: "hello" && Table ID: "hello_id" (as a UUID)
- **Example 1:** Table Name: "hello" && Table ID: "hello_id" (as a UUID)
- **Example 2:** Table Name: "map" && Table ID: "map_id" (as a UUID)
- **Example 2:** Table Name: "map" && Table ID: "map_id" (as a UUID)
2. You should be mapping objects as foreign keys to their respective UUID. For example if you made a new table that holds a player's messages, it would be something like:
- From: player_id
- To: Player.id
- From: player_id
- To: Player.id
### Updating the GraphQL schemas
@@ -154,17 +156,16 @@ By default, only admins are allowed to change the permissions. In order to query
```json
{
"id": {
"_eq":"X-Hasura-User-Id"
}
"id": {
"_eq": "X-Hasura-User-Id"
}
}
```
5. Furthermore, when selecting or updating data. You can add permissions for specific columns. You select which ones should be allowed via the provided checkboxes.
6. Finally, make sure that the changes for permissions are updated in `hasura/metadata/tables.yaml`.
`Pre-update check`nsert and read data immediately. There are pre-generated functions that come with Hasura. For creating new entries. The following are example queries you could send immediately to `http://localhost:8080/v1/graphql`.
`Pre-update check`nsert and read data immediately. There are pre-generated functions that come with Hasura. For creating new entries. The following are example queries you could send immediately to `http://localhost:8080/v1/graphql`.
### Mutations and Querying
@@ -178,9 +179,7 @@ mutation insert {
# Built in function with Hasura GraphQL
insert_Item_one(
# The respective columns
object: {
data: "..."
}
object: { data: "..." }
) {
# The keys to return on a successful insertion
id
@@ -195,20 +194,19 @@ As long as it fits the constraints of the table (ie: Foreign Key uniqueness, Dat
```graphql
mutation update {
# Built in Hasura function
update_Item(
where: {
# Can also use _gt, _gte, _lt, _lte, _neq etc.
# Can also specify any column
id: {_eq: "[UUID]"}
},
# The columns you want to update
_set: {
data: "..."
}) {
# you can either supply `returning` or `affected_rows` for the response
affected_rows
# Built in Hasura function
update_Item(
where: {
# Can also use _gt, _gte, _lt, _lte, _neq etc.
# Can also specify any column
id: { _eq: "[UUID]" }
}
# The columns you want to update
_set: { data: "..." }
) {
# you can either supply `returning` or `affected_rows` for the response
affected_rows
}
}
```
@@ -231,15 +229,16 @@ mutation updateByKey {
```graphql
mutation delete {
delete_Item(
where: {
# Can also use _gt, _gte, _lt, _lte, _neq etc.
# Can also specify any column
id: {_eq: "[UUID]"}
}) {
# you can either supply `returning` or `affected_rows` for the response
affected_rows
delete_Item(
where: {
# Can also use _gt, _gte, _lt, _lte, _neq etc.
# Can also specify any column
id: { _eq: "[UUID]" }
}
) {
# you can either supply `returning` or `affected_rows` for the response
affected_rows
}
}
```
@@ -247,11 +246,11 @@ You can also delete by `id` as well too.
```graphql
mutation deleteByKey {
delete_Item_by_pk(id: "[UUID]") {
# Any columns that exist on the `Item` table
id
...data
}
delete_Item_by_pk(id: "[UUID]") {
# Any columns that exist on the `Item` table
id
...data
}
}
```
@@ -259,27 +258,27 @@ mutation deleteByKey {
```graphql
query get {
Item(
where: {
# Can also use _gt, _gte, _lt, _lte, _neq etc.
# Can also specify any column
id: {_eq: "[UUID]"}
},
# Maximum number of results
limit: 10,
# Pagination
offset: 0,
# Sorting
order_by: {
# asc - ascending, desc - descending
# Can also specify any column
id: asc
}
) {
# The keys to return on a successful query
id
data
Item(
where: {
# Can also use _gt, _gte, _lt, _lte, _neq etc.
# Can also specify any column
id: { _eq: "[UUID]" }
}
# Maximum number of results
limit: 10
# Pagination
offset: 0
# Sorting
order_by: {
# asc - ascending, desc - descending
# Can also specify any column
id: asc
}
) {
# The keys to return on a successful query
id
data
}
}
```
@@ -287,11 +286,11 @@ You can also query by `id` as well too.
```graphql
query getByKey {
Item_by_pk(id: "[UUID]") {
# The keys to return on a successful query
id,
...data
}
Item_by_pk(id: "[UUID]") {
# The keys to return on a successful query
id
...data
}
}
```

View File

@@ -33,6 +33,11 @@ export {
VStack,
useTheme,
useToast,
Modal,
ModalContent,
ModalCloseButton,
ModalOverlay,
useDisclosure,
} from '@chakra-ui/core';
export { theme as MetaTheme } from './theme';

View File

@@ -14,6 +14,7 @@ interface MetaColors {
gold: string;
silver: string;
bronze: string;
purple80: string;
}
interface MetaTheme {
@@ -83,6 +84,7 @@ export const theme: Theme = {
bronze: '#a97142',
offwhite: '#F6F8F9',
blue20: 'rgba(79, 105, 205, 0.2)',
purple80: 'rgba(70, 20, 100, 0.8)',
dark: '#1B0D2A',
purpleBoxDark: '#261943',
purpleBoxLight: '#392373',

View File

@@ -29,7 +29,7 @@ export const PlayerBox: React.FC<PlayerBoxProps> = ({
{title}
</Text>
<FaTimes
color="#A5B9F6"
color="blueLight"
opacity="0.4"
cursor="pointer"
onClick={setRemoveBox}

View File

@@ -31,7 +31,7 @@ export const PlayerCollab: React.FC<Props> = ({ player }) => {
timezone
</Text>
<HStack alignItems="baseline">
<FaGlobe color="#A5B9F6" />
<FaGlobe color="blueLight" />
<Text fontSize="xl" mb="1">
{player.box_profile?.location || '-'}
</Text>
@@ -49,7 +49,7 @@ export const PlayerCollab: React.FC<Props> = ({ player }) => {
Availability
</Text>
<HStack alignItems="baseline">
<FaClock color="#A5B9F6" />
<FaClock color="blueLight" />
<Text fontSize="xl" mb="1">
{player.availability_hours || '0'}h/week
</Text>

View File

@@ -1,63 +1,138 @@
import { Box, Flex, HStack, Text } from '@metafam/ds';
import {
Box,
Flex,
Heading,
HStack,
Modal,
ModalContent,
ModalOverlay,
SimpleGrid,
Text,
useDisclosure,
} from '@metafam/ds';
import { MetaLink as Link } from 'components/Link';
import { PlayerFragmentFragment } from 'graphql/autogen/types';
import {
Collectible,
useOpenSeaCollectibles,
} from 'lib/useOpenSeaCollectibles';
import React from 'react';
import { FaTimes } from 'react-icons/fa';
import nft1Image from '../../../assets/fake/nft1.png';
import nft2Image from '../../../assets/fake/nft2.png';
import { PlayerBox } from './PlayerBoxe';
// TODO Fake data
type Props = { setRemoveBox: () => void };
export const PlayerGallery: React.FC<Props> = ({ setRemoveBox }) => {
const [show, setShow] = React.useState(false);
const fakeEthPrice = 500;
const fakeData = [
{ title: 'CryptoMon Cards - Aave', priceInEth: 0.025, img: nft1Image },
{ title: 'metagamer', priceInEth: 6.942, img: nft2Image },
];
type Props = { player: PlayerFragmentFragment; setRemoveBox: () => void };
const GalleryItem: React.FC<{ nft: Collectible; noMargin?: boolean }> = ({
nft,
noMargin = false,
}) => (
<Link
href={nft.openseaLink}
isExternal
mb={noMargin ? undefined : 6}
minW={0}
display="flex"
>
<HStack spacing={6}>
<Flex width="7.5rem" height="7.5rem">
<Box
bgImage={`url(${nft.imageUrl})`}
backgroundSize="contain"
backgroundRepeat="no-repeat"
backgroundPosition="center"
w="7.5rem"
h="7.5rem"
m="auto"
/>
</Flex>
<Flex direction="column">
<Heading
fontSize="xs"
mt={3}
mb={3}
textTransform="uppercase"
display="inline-block"
style={{ wordWrap: 'break-word', wordBreak: 'break-all' }}
>
{nft.title}
</Heading>
<Text fontSize="sm">{nft.priceString}</Text>
</Flex>
</HStack>
</Link>
);
export const PlayerGallery: React.FC<Props> = ({ player, setRemoveBox }) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const { favorites, data, loading } = useOpenSeaCollectibles({ player });
return (
<PlayerBox title="Gallery" setRemoveBox={setRemoveBox}>
{(fakeData || []).slice(0, show ? 999 : 3).map((nft) => (
<HStack alignItems="end" mb={6}>
<Flex width="126px" height="126px" mr={6}>
<Box
bgImage={`url(${nft.img})`}
backgroundSize="contain"
backgroundRepeat="no-repeat"
backgroundPosition="center"
width="124px"
height="124px"
m="auto"
/>
</Flex>
<Box>
<Text
fontSize="xs"
fontFamily="heading"
mt={3}
mb={3}
casing="uppercase"
>
{nft.title}
</Text>
<Text fontSize="sm" mb="1">
{nft.priceInEth}Ξ ($
{parseFloat(`${nft.priceInEth * fakeEthPrice}`).toFixed(2)})
</Text>
</Box>
</HStack>
))}
{(fakeData || []).length > 3 && (
{!loading &&
favorites?.map((nft) => <GalleryItem nft={nft} key={nft.tokenId} />)}
{!loading && data?.length > 3 && (
<Text
as="span"
fontFamily="body"
fontSize="xs"
color="cyanText"
cursor="pointer"
onClick={() => setShow(!show)}
onClick={onOpen}
>
View {show ? 'less' : 'all'}
View all
</Text>
)}
<Modal
isOpen={isOpen}
onClose={onClose}
isCentered
scrollBehavior="inside"
>
<ModalOverlay css={{ backdropFilter: 'blur(8px)' }}>
<ModalContent maxW="6xl" bg="none">
<Box bg="purple80" borderTopRadius="lg" p={4} w="100%">
<HStack>
<Text
fontFamily="mono"
fontSize="sm"
fontWeight="bold"
color="blueLight"
as="div"
mr="auto"
>
Gallery
</Text>
<FaTimes
color="blueLight"
opacity="0.4"
cursor="pointer"
onClick={onClose}
/>
</HStack>
</Box>
<Box
overflowY="scroll"
overflowX="hidden"
maxH="80vh"
borderBottomRadius="lg"
w="100%"
>
<SimpleGrid
columns={{ base: 1, md: 2, lg: 3 }}
gap={6}
padding={6}
boxShadow="md"
bg="whiteAlpha.200"
css={{ backdropFilter: 'blur(8px)' }}
>
{data?.map((nft) => (
<GalleryItem nft={nft} key={nft.tokenId} noMargin />
))}
</SimpleGrid>
</Box>
</ModalContent>
</ModalOverlay>
</Modal>
</PlayerBox>
);
};

View File

@@ -3,4 +3,5 @@ export const CONFIG = {
process.env.NEXT_PUBLIC_GRAPHQL_URL || 'http://localhost:8080/v1/graphql',
infuraId:
process.env.NEXT_PUBLIC_INFURA_ID || '781d8466252d47508e177b8637b1c2fd',
openseaApiKey: process.env.NEXT_OPENSEA_API_KEY || undefined,
};

View File

@@ -38,6 +38,10 @@ export const PlayerFragment = gql`
job
location
name
collectiblesFavorites {
tokenId
address
}
}
daohausMemberships {
id

View File

@@ -0,0 +1,139 @@
import { CONFIG } from 'config';
import { utils } from 'ethers';
import { PlayerFragmentFragment } from 'graphql/autogen/types';
import { OpenSeaAPI } from 'opensea-js';
import {
AssetEvent,
OpenSeaAsset,
OpenSeaAssetQuery,
} from 'opensea-js/lib/types';
import { useEffect, useMemo, useState } from 'react';
const opensea = new OpenSeaAPI({ apiKey: CONFIG.openseaApiKey });
type OpenSeaCollectiblesOpts = {
player: PlayerFragmentFragment;
};
export type Collectible = {
address: string;
tokenId: string;
title: string;
imageUrl: string;
openseaLink: string;
priceString: string;
};
export const useOpenSeaCollectibles = ({
player,
}: OpenSeaCollectiblesOpts): {
favorites: Array<Collectible>;
data: Array<Collectible>;
loading: boolean;
} => {
const [favorites, setFavorites] = useState<Array<Collectible>>([]);
const [data, setData] = useState<Array<Collectible>>([]);
const [loading, setLoading] = useState<boolean>(false);
const owner = player.ethereum_address;
const collectiblesFavorites = useMemo(
() =>
player && player.box_profile && player.box_profile.collectiblesFavorites
? player.box_profile.collectiblesFavorites
: [],
[player],
);
useEffect(() => {
async function load() {
setLoading(true);
if (collectiblesFavorites.length > 0 && owner) {
const favoritesQuery = {
owner,
token_ids: collectiblesFavorites.map(({ tokenId }) => tokenId || ''),
};
const [favoritesData, allData] = await Promise.all([
fetchOpenSeaData(favoritesQuery),
fetchAllOpenSeaData(owner),
]);
setFavorites(favoritesData);
setData(allData);
} else if (owner) {
const allData = await fetchAllOpenSeaData(owner);
setData(allData);
setFavorites(allData.slice(0, 3));
}
setLoading(false);
}
load();
}, [collectiblesFavorites, owner]);
return { favorites, data, loading };
};
const fetchAllOpenSeaData = async (
owner: string,
): Promise<Array<Collectible>> => {
let offset = 0;
let data: Array<Collectible> = [];
let lastData: Array<Collectible> = [];
do {
const query = { owner, offset, limit: 50 };
// eslint-disable-next-line no-await-in-loop
lastData = await fetchOpenSeaData(query);
data = data.concat(lastData);
offset += 50;
} while (lastData.length > 0);
return data;
};
const fetchOpenSeaData = async (
query: OpenSeaAssetQuery,
): Promise<Array<Collectible>> => {
const response = await opensea.getAssets(query);
return parseAssets(response.assets);
};
const parseAssets = async (
assets: Array<OpenSeaAsset>,
): Promise<Array<Collectible>> => {
return assets
.map(
(asset) =>
({
address: asset.assetContract.address,
tokenId: asset.tokenId,
title: asset.name,
imageUrl: asset.imageUrl,
openseaLink: asset.openseaLink,
priceString: getPriceString(asset.lastSale),
} as Collectible),
)
.filter(
(collectible: Collectible) =>
!!collectible.title && !!collectible.imageUrl,
);
};
const ETH_ADDRESSES = [
'0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', // wETH
'0x0000000000000000000000000000000000000000', // ETH
];
const getPriceString = (event: AssetEvent | null): string => {
if (event && event.paymentToken) {
const {
address,
symbol: tokenSymbol,
decimals,
usdPrice,
} = event.paymentToken;
const symbol = ETH_ADDRESSES.indexOf(address) === -1 ? tokenSymbol : 'Ξ';
const price = Number(utils.formatUnits(event.totalPrice, decimals));
const priceInUSD = usdPrice ? price * Number(usdPrice) : 0;
return `${price.toFixed(2)}${symbol}${
priceInUSD ? ` ($${priceInUSD.toFixed(2)})` : ''
}`;
}
return '';
};

View File

@@ -22,6 +22,7 @@
"next": "latest",
"next-images": "^1.4.1",
"next-urql": "2.0.0",
"opensea-js": "^1.1.10",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-icons": "^3.11.0",

View File

@@ -34,7 +34,7 @@ const PlayerPage: React.FC<Props> = ({ player }) => {
const [fakeData, setFakeData] = React.useState([
[BOX_TYPE.PLAYER_SKILLS, BOX_TYPE.PLAYER_CONTACT_BUTTONS],
[BOX_TYPE.PLAYER_MEMBERSHIPS],
[],
[BOX_TYPE.PLAYER_GALLERY],
]);
if (!player) {
@@ -74,7 +74,12 @@ const PlayerPage: React.FC<Props> = ({ player }) => {
/>
);
case BOX_TYPE.PLAYER_GALLERY:
return <PlayerGallery setRemoveBox={() => removeBox(column, name)} />;
return (
<PlayerGallery
player={player}
setRemoveBox={() => removeBox(column, name)}
/>
);
case BOX_TYPE.PLAYER_MEMBERSHIPS:
return (
<PlayerMemberships
@@ -155,7 +160,9 @@ const PlayerPage: React.FC<Props> = ({ player }) => {
ml={[0, null, null, 4]}
>
{(fakeData || [[], [], []])[2].map((name) => (
<Box mb="6">{getBox(2, name)}</Box>
<Box mb="6" key={name}>
{getBox(2, name)}
</Box>
))}
<Box mb="6">
<PlayerAddBox

1850
yarn.lock

File diff suppressed because it is too large Load Diff