Implement list of Guilds in frontend

This commit is contained in:
Hammad Jutt
2020-12-20 16:59:29 -07:00
parent 94df414920
commit 704bd38cce
23 changed files with 1293 additions and 128 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,12 @@
import { Button, ButtonProps } from '@chakra-ui/core';
import React from 'react';
export const MetaButton: React.FC<ButtonProps> = ({ children, ...props }) => (
type LinkProps = { href: string; target: '_blank' };
export const MetaButton: React.FC<ButtonProps & LinkProps> = ({
children,
...props
}) => (
<Button
colorScheme="purple"
textTransform="uppercase"

View File

@@ -13,6 +13,8 @@ interface MetaColors {
platinum: string;
gold: string;
silver: string;
discord: string;
discordDark: string;
bronze: string;
purple80: string;
}
@@ -91,6 +93,8 @@ export const theme: Theme = {
purpleTag: '#40347C',
blueLight: '#A5B9F6',
cyanText: '#79F8FB',
discord: '#7289da',
discordDark: '#5d6eb3',
},
fonts: {
body: '"IBM Plex Sans", sans-serif',

View File

@@ -0,0 +1,42 @@
import { Avatar, Box, MetaButton, Text, VStack } from '@metafam/ds';
import { GuildFragmentFragment } from 'graphql/autogen/types';
import React from 'react';
import { ProfileSection } from '../ProfileSection';
import { GuildLinks } from './GuildLinks';
type Props = { guild: GuildFragmentFragment };
export const GuildHero: React.FC<Props> = ({ guild }) => {
return (
<ProfileSection>
<VStack spacing={8}>
{guild.logo ? (
<Avatar
w={{ base: '32', md: '56' }}
h={{ base: '32', md: '56' }}
src={guild.logo}
name={guild.name}
/>
) : null}
<Box textAlign="center">
<Text fontSize="xl" fontFamily="heading" mb="1">
{guild.name}
</Text>
<Text fontFamily="mono" fontSize="md" color="blueLight">
{`${guild.type} GUILD`}
</Text>
</Box>
<Box>
<Text>{guild.description}</Text>
</Box>
{guild.join_button_url ? (
<MetaButton as="a" href={guild.join_button_url} target="_blank">
Join
</MetaButton>
) : null}
<GuildLinks guild={guild} />
</VStack>
</ProfileSection>
);
};

View File

@@ -0,0 +1,67 @@
import { Button, Wrap } from '@metafam/ds';
import { GuildFragmentFragment } from 'graphql/autogen/types';
import React from 'react';
import { FaDiscord, FaGlobe } from 'react-icons/fa';
type Props = {
guild: GuildFragmentFragment;
};
export const GuildLinks: React.FC<Props> = ({ guild }) => {
return (
<Wrap>
{guild.website_url ? (
<Button
as="a"
href={guild.website_url}
target="_blank"
size="xs"
colorScheme="blackAlpha"
leftIcon={<FaGlobe />}
>
Website
</Button>
) : null}
{guild.discord_invite_url ? (
<Button
as="a"
href={guild.discord_invite_url}
target="_blank"
size="xs"
bgColor="discord"
_hover={{ bgColor: 'discordDark' }}
leftIcon={<FaDiscord />}
>
Discord
</Button>
) : null}
{/*
{guild.Accounts.map((acc) => {
if (acc.type === 'TWITTER') {
const link = `https://twitter.com/${acc.identifier}`;
return (
);
}
if (acc.type === 'GITHUB') {
const link = `https://github.com/${acc.identifier}`;
return (
<Button
as="a"
href={link}
target="_blank"
key={link}
size="xs"
colorScheme="blackAlpha"
backgroundColor="black"
leftIcon={<FaGithub />}
>
{acc.identifier}
</Button>
);
}
return null;
})} */}
</Wrap>
);
};

View File

@@ -0,0 +1,78 @@
import {
Avatar,
Flex,
Heading,
HStack,
MetaButton,
MetaTag,
Text,
VStack,
Wrap,
} from '@metafam/ds';
import { GuildLinks } from 'components/Guild/GuildLinks';
import { GuildFragmentFragment } from 'graphql/autogen/types';
import React from 'react';
type Props = {
guild: GuildFragmentFragment;
};
export const GuildTile: React.FC<Props> = ({ guild }) => (
<Flex
direction="column"
key={guild.id}
bg="whiteAlpha.200"
style={{ backdropFilter: 'blur(7px)' }}
rounded="lg"
p="6"
maxW="30rem"
w="100%"
align="stretch"
position="relative"
overflow="hidden"
justify="space-between"
>
<VStack w="100%" spacing="6" align="stretch" mb={6} position="relative">
<VStack>
{guild.logo ? (
<Avatar size="xl" src={guild.logo} name={guild.name} />
) : null}
<Heading size="sm" color="white">
{guild.name}
</Heading>
</VStack>
<Wrap w="100%" justify="center">
{guild.type ? <MetaTag size="md">{guild.type} GUILD</MetaTag> : null}
</Wrap>
{guild.description ? (
<VStack spacing={2} align="stretch">
<Text fontFamily="mono" fontSize="sm" color="blueLight">
ABOUT
</Text>
<Text fontSize="sm">{guild.description}</Text>
</VStack>
) : null}
</VStack>
<VStack w="100%" spacing="6" align="stretch">
<VStack spacing={2} align="stretch">
<Text fontFamily="mono" fontSize="sm" color="blueLight">
LINKS
</Text>
<HStack mt="2">
<GuildLinks guild={guild} />
</HStack>
</VStack>
{guild.join_button_url ? (
<MetaButton
as="a"
href={guild.join_button_url}
target="_blank"
fontFamily="mono"
>
Join
</MetaButton>
) : null}
</VStack>
</Flex>
);

View File

@@ -0,0 +1,16 @@
import { SimpleGrid } from '@metafam/ds';
import { GuildTile } from 'components/Guild/GuildTile';
import { GuildFragmentFragment } from 'graphql/autogen/types';
import React from 'react';
type Props = {
guilds: GuildFragmentFragment[];
};
export const GuildList: React.FC<Props> = ({ guilds }) => (
<SimpleGrid columns={[1, null, 2, 3]} spacing="8">
{guilds.map((p) => (
<GuildTile key={p.id} guild={p} />
))}
</SimpleGrid>
);

View File

@@ -76,7 +76,7 @@ export const PageHeader: React.FC = () => {
direction={{ base: 'column', md: 'row' }}
spacing={{ base: 4, md: 6, lg: 10 }}
>
<MenuItem href="/">Players</MenuItem>
<MenuItem href="/guilds">Guilds</MenuItem>
<MenuItem href="https://discord.gg/VYZPBnx" isExternal>
Discord
</MenuItem>

View File

@@ -1,29 +0,0 @@
import { Box, Text } from '@metafam/ds';
import React from 'react';
type Props = {
title: string;
value?: string | null | undefined;
} & React.ComponentProps<typeof Text>;
export const PlayerFeature: React.FC<Props> = ({
title,
value,
children,
...props
}) => (
<Box>
<Text
fontFamily="body"
fontSize="sm"
color="blueLight"
textTransform="uppercase"
mb="2"
>
{title}
</Text>
<Text fontFamily="body" fontSize="md" fontWeight="bold" {...props}>
{value || children}
</Text>
</Box>
);

View File

@@ -1,58 +0,0 @@
import { Container, Divider, MetaTag, Wrap } from '@metafam/ds';
import { PlayerFeature } from 'components/Player/PlayerFeature';
import { PlayerFragmentFragment } from 'graphql/autogen/types';
import React from 'react';
type Props = { player: PlayerFragmentFragment };
export const PlayerFeatures: React.FC<Props> = ({ player }) => {
return (
<Container maxW="xl">
<Wrap
spacing="8"
ml={{ base: '4', lg: '64' }}
pt={{ base: '4', md: '12', lg: 0 }}
>
<PlayerFeature
title="XP"
value={Math.floor(player.totalXp).toString()}
/>
{player.rank && (
<Divider orientation="vertical" color="whiteAlpha.400" />
)}
{player.rank && (
<PlayerFeature title="Rank">
<MetaTag
backgroundColor={player.rank?.toLowerCase()}
size="md"
color="blackAlpha.600"
>
{player.rank}
</MetaTag>
</PlayerFeature>
)}
{player.box_profile?.location && (
<Divider orientation="vertical" color="whiteAlpha.400" />
)}
{player.box_profile?.location && (
<PlayerFeature
title="Location"
value={player.box_profile?.location}
/>
)}
<Divider orientation="vertical" color="whiteAlpha.400" />
{/* <PlayerFeature title="Role" value="N/A" color="whiteAlpha.500" /> */}
{/* <Divider orientation="vertical" color="whiteAlpha.400" /> */}
<PlayerFeature
title="Availability"
value={
player.availability_hours != null
? `${player.availability_hours} hr / week`
: 'N/A'
}
color={player.availability_hours ? undefined : 'whiteAlpha.500'}
/>
</Wrap>
</Container>
);
};

View File

@@ -2,7 +2,7 @@ import { HStack, Text } from '@metafam/ds';
import React from 'react';
import { FaMedal } from 'react-icons/fa';
import { PlayerSection } from './PlayerSection';
import { ProfileSection } from '../../ProfileSection';
// TODO Fake data
type Props = { onRemoveClick: () => void };
@@ -15,7 +15,7 @@ export const PlayerAchievements: React.FC<Props> = ({ onRemoveClick }) => {
];
return (
<PlayerSection title="Achievements" onRemoveClick={onRemoveClick}>
<ProfileSection title="Achievements" onRemoveClick={onRemoveClick}>
{(fakeData || []).slice(0, show ? 999 : 3).map((title) => (
<HStack alignItems="baseline" mb={3}>
<FaMedal color="#FBB112" />
@@ -34,6 +34,6 @@ export const PlayerAchievements: React.FC<Props> = ({ onRemoveClick }) => {
View {show ? 'less' : 'all'}
</Text>
)}
</PlayerSection>
</ProfileSection>
);
};

View File

@@ -9,7 +9,7 @@ export const PlayerCollab: React.FC<Props> = ({ player }) => {
<HStack
spacing={6}
divider={
<Divider height="3rem" color="purpleTag" orientation="vertical" />
<Divider height="3rem" color="whiteAlpha.400" orientation="vertical" />
}
>
<Box>

View File

@@ -19,7 +19,7 @@ import {
import React from 'react';
import { FaTimes } from 'react-icons/fa';
import { PlayerSection } from './PlayerSection';
import { ProfileSection } from '../../ProfileSection';
type Props = { player: PlayerFragmentFragment; onRemoveClick: () => void };
@@ -67,7 +67,7 @@ export const PlayerGallery: React.FC<Props> = ({ player, onRemoveClick }) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const { favorites, data, loading } = useOpenSeaCollectibles({ player });
return (
<PlayerSection title="NFT Gallery" onRemoveClick={onRemoveClick}>
<ProfileSection title="NFT Gallery" onRemoveClick={onRemoveClick}>
{!loading &&
favorites?.map((nft) => <GalleryItem nft={nft} key={nft.tokenId} />)}
{!loading && data?.length > 3 && (
@@ -133,6 +133,6 @@ export const PlayerGallery: React.FC<Props> = ({ player, onRemoveClick }) => {
</ModalContent>
</ModalOverlay>
</Modal>
</PlayerSection>
</ProfileSection>
);
};

View File

@@ -9,20 +9,20 @@ import {
import { PersonalityTypes } from '../../../graphql/types';
import { FlexContainer } from '../../Container';
import { ProfileSection } from '../../ProfileSection';
import { PlayerContacts } from '../PlayerContacts';
import { PlayerCollab } from './PlayerCollab';
import { PlayerSection } from './PlayerSection';
const BIO_LENGTH = 240;
type Props = { player: PlayerFragmentFragment };
export const PlayerHero: React.FC<Props> = ({ player }) => {
const [show, setShow] = React.useState(
getPlayerDescription(player).length < 115,
getPlayerDescription(player).length < BIO_LENGTH,
);
return (
<PlayerSection>
<ProfileSection>
<VStack spacing={8}>
<Avatar
w={{ base: '32', md: '56' }}
@@ -34,12 +34,7 @@ export const PlayerHero: React.FC<Props> = ({ player }) => {
<Text fontSize="xl" fontFamily="heading" mb="1">
{getPlayerName(player)}
</Text>
<HStack mt="2">
<PlayerContacts player={player} />
</HStack>
</Box>
<Box>
<Text>
{`${getPlayerDescription(player).substring(
@@ -60,6 +55,10 @@ export const PlayerHero: React.FC<Props> = ({ player }) => {
)}
</Text>
</Box>
<HStack mt="2">
<PlayerContacts player={player} />
</HStack>
<Box w="100%">
<PlayerCollab player={player} />
</Box>
@@ -89,6 +88,6 @@ export const PlayerHero: React.FC<Props> = ({ player }) => {
</FlexContainer>
) : null}
</VStack>
</PlayerSection>
</ProfileSection>
);
};

View File

@@ -8,7 +8,7 @@ import metacartelImage from '../../../assets/moloch/metacartel.png';
import metaclanImage from '../../../assets/moloch/metaclan.png';
import metagameImage from '../../../assets/moloch/metagame.png';
import raidGuildImage from '../../../assets/moloch/raid_guild.png';
import { PlayerSection } from './PlayerSection';
import { ProfileSection } from '../../ProfileSection';
type Props = { player: PlayerFragmentFragment; onRemoveClick: () => void };
export const PlayerMemberships: React.FC<Props> = ({
@@ -27,7 +27,7 @@ export const PlayerMemberships: React.FC<Props> = ({
};
return (
<PlayerSection title="Memberships" onRemoveClick={onRemoveClick}>
<ProfileSection title="Memberships" onRemoveClick={onRemoveClick}>
<HStack alignItems="center" mb={6}>
<Flex bg="purpleBoxLight" width={16} height={16} mr={6}>
<Box
@@ -94,6 +94,6 @@ export const PlayerMemberships: React.FC<Props> = ({
View {show ? 'less' : 'all'}
</Text>
)}
</PlayerSection>
</ProfileSection>
);
};

View File

@@ -3,7 +3,7 @@ import { PlayerFragmentFragment } from 'graphql/autogen/types';
import { SkillColors } from 'graphql/types';
import React from 'react';
import { PlayerSection } from './PlayerSection';
import { ProfileSection } from '../../ProfileSection';
type Props = { player: PlayerFragmentFragment; onRemoveClick: () => void };
export const PlayerSkills: React.FC<Props> = ({ player, onRemoveClick }) => {
@@ -11,7 +11,7 @@ export const PlayerSkills: React.FC<Props> = ({ player, onRemoveClick }) => {
return null;
}
return (
<PlayerSection title="Skills" onRemoveClick={onRemoveClick}>
<ProfileSection title="Skills" onRemoveClick={onRemoveClick}>
<Wrap>
{(player.Player_Skills || []).map(({ Skill }) => (
<MetaTag
@@ -28,6 +28,6 @@ export const PlayerSkills: React.FC<Props> = ({ player, onRemoveClick }) => {
</MetaTag>
))}
</Wrap>
</PlayerSection>
</ProfileSection>
);
};

View File

@@ -2,7 +2,7 @@ import { Box, HStack, Text } from '@metafam/ds';
import React from 'react';
import { FaTimes } from 'react-icons/fa';
export type PlayerSectionProps = {
export type ProfileSectionProps = {
title?: string;
children?: React.ReactNode;
onRemoveClick?: () => void;
@@ -10,7 +10,7 @@ export type PlayerSectionProps = {
};
// TODO If MetaBox is only used for Player profile maybe merge both component
export const PlayerSection: React.FC<PlayerSectionProps> = ({
export const ProfileSection: React.FC<ProfileSectionProps> = ({
children,
title,
onRemoveClick,

View File

@@ -53,3 +53,18 @@ export const PlayerFragment = gql`
}
}
`;
export const GuildFragment = gql`
fragment GuildFragment on Guild {
id
guildname
description
discord_invite_url
join_button_url
logo
moloch_address
name
type
website_url
}
`;

View File

@@ -0,0 +1,23 @@
import gql from 'fake-tag';
import { GetGuildQuery, GetGuildQueryVariables } from './autogen/types';
import { client } from './client';
import { GuildFragment } from './fragments';
const guildQuery = gql`
query GetGuild($guildname: String!) {
Guild(where: { guildname: { _eq: $guildname } }) {
...GuildFragment
}
}
${GuildFragment}
`;
export const getGuild = async (guildname: string | undefined) => {
if (!guildname) return null;
const { data } = await client
.query<GetGuildQuery, GetGuildQueryVariables>(guildQuery, { guildname })
.toPromise();
return data?.Guild[0];
};

View File

@@ -0,0 +1,30 @@
import gql from 'fake-tag';
import { GetGuildsQuery, GetGuildsQueryVariables } from './autogen/types';
import { client } from './client';
import { GuildFragment } from './fragments';
const guildsQuery = gql`
query GetGuilds($limit: Int) {
Guild(limit: $limit) {
...GuildFragment
}
}
${GuildFragment}
`;
export const getGuilds = async (limit = 50) => {
const { data, error } = await client
.query<GetGuildsQuery, GetGuildsQueryVariables>(guildsQuery, { limit })
.toPromise();
if (!data) {
if (error) {
throw error;
}
return [];
}
return data.Guild;
};

View File

@@ -0,0 +1,88 @@
import { Box, Flex, LoadingState, Stack } from '@metafam/ds';
import { getGuild } from 'graphql/getGuild';
import { getGuilds } from 'graphql/getGuilds';
import {
GetStaticPaths,
GetStaticPropsContext,
InferGetStaticPropsType,
} from 'next';
import Error from 'next/error';
import { useRouter } from 'next/router';
import React from 'react';
import { PageContainer } from '../../components/Container';
import { GuildHero } from '../../components/Guild/GuildHero';
import { ProfileSection } from '../../components/ProfileSection';
type Props = InferGetStaticPropsType<typeof getStaticProps>;
const GuildPage: React.FC<Props> = ({ guild }) => {
const router = useRouter();
if (router.isFallback) {
return <LoadingState />;
}
if (!guild) {
return <Error statusCode={404} />;
}
return (
<PageContainer>
<Stack
spacing={6}
align="center"
direction={{ base: 'column', lg: 'row' }}
alignItems="flex-start"
maxWidth="7xl"
>
<Flex flex={1} d="column">
<Box mb="6">
<GuildHero guild={guild} />
</Box>
<Box mb="6">
<ProfileSection />
</Box>
</Flex>
<Flex flex={2}>
<Flex
align="center"
direction={{ base: 'column', lg: 'row' }}
alignItems="flex-start"
>
<ProfileSection />
</Flex>
</Flex>
</Stack>
</PageContainer>
);
};
export default GuildPage;
type QueryParams = { guildname: string };
export const getStaticPaths: GetStaticPaths<QueryParams> = async () => {
const guilds = await getGuilds();
return {
paths: guilds.map(({ guildname }) => ({
params: { guildname },
})),
fallback: true,
};
};
export const getStaticProps = async (
context: GetStaticPropsContext<QueryParams>,
) => {
const guildname = context.params?.guildname;
const guild = await getGuild(guildname);
return {
props: {
guild,
},
revalidate: 1,
};
};

View File

@@ -0,0 +1,25 @@
import { PageContainer } from 'components/Container';
import { GuildList } from 'components/GuildList';
import { getGuilds } from 'graphql/getGuilds';
import { InferGetStaticPropsType } from 'next';
import React from 'react';
type Props = InferGetStaticPropsType<typeof getStaticProps>;
export const getStaticProps = async () => {
const guilds = await getGuilds();
return {
props: {
guilds,
},
revalidate: 10,
};
};
const GuildsPage: React.FC<Props> = ({ guilds }) => (
<PageContainer>
<GuildList guilds={guilds} />
</PageContainer>
);
export default GuildsPage;

View File

@@ -95,7 +95,6 @@ const PlayerPage: React.FC<Props> = ({ player }) => {
};
return (
// TODO Should be a custom background and maybe on app level(with the header)
<PageContainer>
<Flex
align="center"