Skip to content

Commit

Permalink
Homepage: Build user stats preview in dashboard (#6121)
Browse files Browse the repository at this point in the history
* build avatar and profile banner into Dashboard

* Build the rest of a Dashboard component

* add a temporary statsPreview object to DashboardContainer

* fix UserHome import

* add round to top of Dashboard

* add responsive styling to avatar

* fetch user resource via panoptes in DashboardContainer

* import UserHome into lib-user dev App

* fix merge conflicts

* fetch data for StatsTabs

* Fix a few styling things

* Add StyledStatsLink focus styling

---------

Co-authored-by: Mark Bouslog <mcbouslog@gmail.com>
Co-authored-by: Mark Bouslog <mark@zooniverse.org>
  • Loading branch information
3 people committed Jun 13, 2024
1 parent b6c0a3c commit f1e524f
Show file tree
Hide file tree
Showing 7 changed files with 445 additions and 33 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { Box, Image, ResponsiveContext } from 'grommet'
import styled from 'styled-components'
import { bool, shape, string } from 'prop-types'
import { Anchor, Box, Image, ResponsiveContext, Text } from 'grommet'
import { Bookmark, Chat, Favorite, FormNext, MailOption } from 'grommet-icons'
import { useContext } from 'react'
import styled, { css } from 'styled-components'
import { bool, shape, string } from 'prop-types'
import { SpacedHeading, SpacedText } from '@zooniverse/react-components'

import DashboardLink from './components/DashboardLink.js'
import StatsTabsContainer from './components/StatsTabs/StatsTabsContainer.js'

const Relative = styled(Box)`
position: relative;
Expand All @@ -24,39 +29,174 @@ const StyledAvatar = styled(Image)`
}
`

export default function Dashboard({ isLoading = false, user }) {
const NameContainer = styled(Box)`
position: relative;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
height: 2px;
width: 100%;
${props =>
props.theme.dark
? css`
background: linear-gradient(
90deg,
transparent 0%,
#000000 50%,
transparent 100%
);
`
: css`
background: linear-gradient(
90deg,
transparent 0%,
#a6a7a9 50%,
transparent 100%
);
`}
}
`

const StyledStatsLink = styled(Anchor)`
border-radius: 24px; // Same as HeaderButton
padding: 10px;
width: 240px;
display: flex;
justify-content: flex-end;
position: relative;
${props =>
props.theme.dark
? css`
box-shadow: 0px 1px 4px 0px rgba(0, 0, 0, 0.7);
`
: css`
box-shadow: 0px 1px 4px 0px rgba(0, 0, 0, 0.25);
`}
&:hover {
text-decoration: none;
}
&:focus {
box-shadow: 0 0 2px 2px ${props => props.theme.global.colors[props.theme.global.colors.focus]};
}
`

const StyledBadge = styled(Text)`
position: absolute;
right: 10px;
top: -12px;
padding: 3px 5px;
background: ${props => props.theme.global.colors['neutral-1']};
border-radius: 15px;
`

export default function Dashboard({ user, userLoading }) {
const size = useContext(ResponsiveContext)

return (
<Relative
fill
<Box
align='center'
height={
size !== 'small'
? { min: '270px', max: '270px' }
: { min: '180px', max: '180px' }
}
background={
isLoading || !user?.profile_header
? 'brand'
: { image: `url(${user.profile_header})` }
}
round={size !== 'small' ? { size: '16px', corner: 'top' } : false}
pad={{ bottom: '20px' }}
round={size !== 'small' ? '8px' : false}
elevation={size === 'small' ? 'none' : 'xsmall'}
>
<StyledAvatar
alt='User avatar'
src={
!user?.avatar_src || isLoading
? 'https://www.zooniverse.org/assets/simple-avatar.png'
: user.avatar_src
<Relative
fill
align='center'
height={
size !== 'small'
? { min: '270px', max: '270px' }
: { min: '180px', max: '180px' }
}
background={
!user?.profile_header || userLoading
? 'brand'
: { image: `url(${user.profile_header})` }
}
/>
</Relative>
round={size !== 'small' ? {size: '16px', corner: 'top'} : false}
>
<StyledAvatar
alt='User avatar'
src={
!user?.avatar_src || userLoading
? 'https://www.zooniverse.org/assets/simple-avatar.png'
: user?.avatar_src
}
/>
</Relative>

{/* Name */}
<NameContainer
margin={{ top: '94px', bottom: '20px' }}
align='center'
width='min(100%, 45rem)'
pad={{ bottom: '20px' }}
>
<SpacedHeading
level={1}
size='1.5rem'
color={{ light: 'neutral-1', dark: 'accent-1' }}
textAlign='center'
margin={{ bottom: '10px', top: '0' }}
>
{userLoading ? ' ' : user?.display_name}
</SpacedHeading>
<Text>{userLoading ? ' ' : `@${user?.login}`}</Text>
</NameContainer>

{/* Links */}
<Box direction='row' gap='medium' margin={{ bottom: '30px' }}>
<DashboardLink
icon={<Favorite size='1rem' />}
text='Favorites'
href={`https://www.zooniverse.org/favorites/${user?.login}`}
/>
<DashboardLink
icon={<Bookmark size='1rem' />}
text='Collections'
href={`https://www.zooniverse.org/collections/${user?.login}`}
/>
<DashboardLink
icon={<Chat size='1rem' />}
text='Comments'
href={`https://www.zooniverse.org/users/${user?.login}`}
/>
<DashboardLink
icon={<MailOption size='1rem' />}
text='Messages'
href={`https://www.zooniverse.org/inbox`}
/>
</Box>

<Box align='center' gap='20px'>
{/* Stats Preview */}
<StatsTabsContainer user={user} />
<Relative fill>
<StyledStatsLink
alignSelf={size === 'small' ? 'center' : 'end'}
href={`/users/${user?.login}/stats`}
label={<SpacedText>More Stats</SpacedText>}
icon={<FormNext />}
reverse
color={{ light: 'dark-5', dark: 'white' }}
gap='large'
/>
<StyledBadge color='white' size='0.75rem' weight='bold'>
NEW
</StyledBadge>
</Relative>
</Box>
</Box>
)
}

Dashboard.propTypes = {
isLoading: bool,
userLoading: bool,
user: shape({
avatar_src: string,
id: string.isRequired,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
import { Box } from 'grommet'

import Dashboard from './Dashboard.js'

export default {
title: 'Components / UserHome / Dashboard',
component: Dashboard
component: Dashboard,
decorators: [ComponentDecorator]
}

function ComponentDecorator(Story) {
return (
<Box
background={{
dark: 'dark-3',
light: 'neutral-6'
}}
>
<Story />
</Box>
)
}

const USER = {
Expand All @@ -29,7 +45,7 @@ export const Default = {
}
}

export const NoAvatarOrBanner = {
export const NoImagesOrStats = {
args: {
user: USER_NO_IMAGES
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { panoptes } from '@zooniverse/panoptes-js'
import useSWR from 'swr'
import { shape, string } from 'prop-types'

import Dashboard from './Dashboard'

Expand All @@ -11,10 +12,11 @@ const SWROptions = {
refreshInterval: 0
}

/* This is a similar pattern to usePanoptesUser hook, but includes the profile_header */
const fetchProfileBanner = async ({ authUser}) => {
const fetchProfileBanner = async ({ authUser }) => {
try {
const { body } = await panoptes.get(`/users/${authUser.id}/?include=profile_header`)
const { body } = await panoptes.get(
`/users/${authUser.id}/?include=profile_header`
)
const user = body.users?.[0]

if (body.linked?.profile_headers?.length) {
Expand All @@ -27,9 +29,21 @@ const fetchProfileBanner = async ({ authUser}) => {
}
}


export default function DashboardContainer({ authUser }) {
const key = { endpoint: '/users/[id]', authUser }
const { data: user, isLoading } = useSWR(key, fetchProfileBanner, SWROptions)
const { data: user, isLoading: userLoading } = useSWR(key, fetchProfileBanner, SWROptions)

return (
<Dashboard
user={user}
userLoading={userLoading}
/>
)
}

return <Dashboard user={user} isLoading={isLoading} />
DashboardContainer.propTypes = {
authUser: shape({
id: string.isRequired,
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { node, string } from 'prop-types'
import { useContext } from 'react'
import { ResponsiveContext } from 'grommet'
import { PlainButton } from '@zooniverse/react-components'
import { Blank } from 'grommet-icons'

function Icon({ icon, text = '' }) {
return (
<Blank role='img' aria-label={text} aria-hidden='false' size='1rem'>
{icon}
</Blank>
)
}

export default function DashboardLink({ href = '', icon, text = '' }) {
const size = useContext(ResponsiveContext)
return (
<>
{size !== 'small' ? (
<PlainButton href={href} text={text} icon={icon} />
) : (
<Icon text={text} icon={icon} />
)}
</>
)
}

DashboardLink.propTypes = {
href: string,
icon: node,
text: string
}
Loading

0 comments on commit f1e524f

Please sign in to comment.