Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Homepage: Create RecentProjects component, refactor data fetching in RecentSubjects #6125

Merged
merged 14 commits into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 35 additions & 18 deletions packages/lib-react-components/src/ProjectCard/ProjectCard.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Box } from 'grommet'
import { string } from 'prop-types'
import { Box, Text } from 'grommet'
import { number, string } from 'prop-types'
import styled from 'styled-components'
import SpacedText from '../SpacedText'

Expand Down Expand Up @@ -42,42 +42,55 @@ const StyledProjectDescription = styled(SpacedText)`
}
`

function cardWidth (size) {
const StyledBadge = styled(Text)`
display: flex;
margin: 5px 5px 5px auto;
border-radius: 50%;
padding: 3px;
background: white;
aspect-ratio: 1 / 1;
text-align: center;
align-items: center;
justify-content: center;
`

function cardWidth(size) {
switch (size) {
case 'small':
return 157;
return 157
case 'medium':
return 189;
return 189
case 'large':
return 220;
return 220
case 'xlarge':
return 252;
return 252
default:
return 189;
return 189
}
}

function cardFontSize (size) {
function cardFontSize(size) {
switch (size) {
case 'small':
return '0.625rem';
return '0.625rem'
case 'medium':
return '0.656rem';
return '0.656rem'
case 'large':
return '0.688rem';
return '0.688rem'
case 'xlarge':
return '0.8rem';
return '0.8rem'
default:
return '0.656rem';
return '0.656rem'
}
}

function ProjectCard ({
function ProjectCard({
badge = undefined,
description = '',
displayName = '',
href = '',
imageSrc = '',
size = 'medium',
size = 'medium'
}) {
return (
<StyledProjectCard
Expand All @@ -87,19 +100,22 @@ function ProjectCard ({
href={href}
round='8px'
cardFontSize={cardFontSize(size)}
height={`${cardWidth(size) * 14 / 11}px`}
height={`${(cardWidth(size) * 14) / 11}px`}
width={`${cardWidth(size)}px`}
>
<Box
className='project-image'
background={{
image: `url(${imageSrc})`,
position: 'top',
size: 'cover',
size: 'cover'
}}
height={`${cardWidth(size)}px`}
round={{ corner: 'top', size: '8px' }}
>
{badge ? <StyledBadge color='black' size='0.75rem' weight='bold'>
{badge}
</StyledBadge> : null}
</Box>
<StyledProjectContent
flex='grow'
Expand Down Expand Up @@ -129,6 +145,7 @@ function ProjectCard ({
}

ProjectCard.propTypes = {
badge: number,
description: string,
displayName: string,
href: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export default {

export const NfnCaliFlowers = {
args: {
badge: 3,
description: 'Using digital images to investigate ​phenological change in a biodiversity hotspot​',
displayName: `Notes from Nature - Capturing California's Flowers`,
imageSrc: 'https://panoptes-uploads.zooniverse.org/project_avatar/0c4cfec1-a15b-468e-9f57-e9133993532d.jpeg',
Expand Down
13 changes: 11 additions & 2 deletions packages/lib-user/src/components/UserHome/UserHome.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import { shape, string } from 'prop-types'
import { useContext } from 'react'
import { Grid, ResponsiveContext } from 'grommet'

import { Layout } from '@components/shared'
import { ContentBox, Layout } from '@components/shared'
import DashboardContainer from './components/Dashboard/DashboardContainer.js'
import RecentProjectsContainer from './components/RecentProjects/RecentProjectsContainer.js'
import RecentSubjectsContainer from './components/RecentSubjects/RecentSubjectsContainer.js'

function UserHome({ authUser }) {
const size = useContext(ResponsiveContext)

return (
<Layout>
<DashboardContainer authUser={authUser}/>
<DashboardContainer authUser={authUser} />
<Grid gap='medium' columns={size !== 'small' ? ['1fr 1fr'] : ['1fr']}>
<RecentProjectsContainer authUser={authUser} />
<ContentBox />
mcbouslog marked this conversation as resolved.
Show resolved Hide resolved
</Grid>
<RecentSubjectsContainer authUser={authUser} />
</Layout>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Anchor, Box, ResponsiveContext, Text } from 'grommet'
import { arrayOf, bool, shape, string } from 'prop-types'
import { useContext } from 'react'
import { Loader, ProjectCard, SpacedText } from '@zooniverse/react-components'

import { ContentBox } from '@components/shared'

export default function RecentProjects({
isLoading = false,
projectPreferences = [],
error = undefined
}) {
const size = useContext(ResponsiveContext)

return (
<ContentBox title='Continue Classifying' screenSize={size}>
{isLoading && (
<Box fill justify='center' align='center'>
<Loader />
</Box>
)}
{!isLoading && error && (
<Box fill justify='center' align='center' pad='medium'>
<SpacedText>
There was an error fetching your recent projects
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consideration, show error.message here

</SpacedText>
</Box>
)}
{!isLoading && !projectPreferences.length && !error && (
<Box fill justify='center' align='center' pad='medium'>
<SpacedText>No Recent Projects found</SpacedText>
<Text>
Start by{' '}
<Anchor href='https://www.zooniverse.org/projects'>
classifying any project
</Anchor>
.
</Text>
</Box>
)}
{!isLoading &&
projectPreferences?.length ? (
<Box
as='ul'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved ul after loading/error/empty states content

direction='row'
gap='small'
pad={{ horizontal: 'xxsmall', bottom: 'xsmall', top: 'xxsmall' }}
overflow={{ horizontal: 'auto' }}
style={{ listStyle: 'none' }}
margin='0'
>
{projectPreferences.map(preference => (
<li key={preference?.project?.id}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added <li>

<ProjectCard
badge={preference?.activity_count}
description={preference?.project?.description}
displayName={preference?.project?.display_name}
href={`https://www.zooniverse.org/projects/${preference?.project?.slug}`}
imageSrc={preference?.project?.avatar_src}
size={size}
/>
</li>
))}
</Box>
) : null}
</ContentBox>
)
}

RecentProjects.propTypes = {
isLoading: bool,
projectPreferences: arrayOf(
shape({
id: string
})
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import RecentProjects from './RecentProjects.js'
import { PROJECTS } from '../../../../../test/mocks/panoptes/projects.js'

const mockProjectPreferencesWithProjectObj = PROJECTS.map(project => ({
activity_count: Math.floor(Math.random() * 100),
project
}))

export default {
title: 'Components / UserHome / RecentProjects',
component: RecentProjects
}

export const Default = {
args: {
projectPreferences: mockProjectPreferencesWithProjectObj
}
}

export const NoProjects = {
args: {
projectPreferences: []
}
}

export const Error = {
args: {
projectPreferences: [],
error: { message: `Couldn't fetch recent projects` }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { shape, string } from 'prop-types'
import { panoptes } from '@zooniverse/panoptes-js'
import useSWR from 'swr'
import auth from 'panoptes-client/lib/auth'

import { usePanoptesProjects } from '@hooks'
import RecentProjects from './RecentProjects.js'

const SWROptions = {
revalidateIfStale: true,
revalidateOnMount: true,
revalidateOnFocus: true,
revalidateOnReconnect: true,
refreshInterval: 0
}

async function fetchUserProjectPreferences() {
const user = auth.checkCurrent()
const token = await auth.checkBearerToken()
const authorization = `Bearer ${token}`
try {
const query = {
page: 1, // returns 20 items
sort: '-updated_at',
user_id: user.id
}
const response = await panoptes.get('/project_preferences', query, { authorization })
if (response.ok) {
const projectPreferencesUserHasClassified =
response.body.project_preferences
.filter(preference => preference.activity_count > 0)
.slice(0, 10)
return projectPreferencesUserHasClassified
}
return []
} catch (error) {
console.error(error)
throw error
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the related components handle error states I think this fetch should throw an error if it has one so that error can be included in the error returned from useSWR on line 51?

}
}

export default function RecentProjectsContainer({ authUser }) {
// Get user's project preference.activity_count for 10 most recently classified projects
const cacheKey = {
name: 'user-project-preferences',
userId: authUser.id
}
const {
data: projectPreferences,
isLoading: preferencesLoading,
error: preferencesError
} = useSWR(cacheKey, fetchUserProjectPreferences, SWROptions)

// Get more info about each project and attach it to correct projectPreference object
const recentProjectIds = projectPreferences?.map(
preference => preference.links.project
)
const {
data: projects,
isLoading: projectsLoading,
error: projectsError
} = usePanoptesProjects(recentProjectIds)

let projectPreferencesWithProjectObj

if (projects?.length) {
projectPreferencesWithProjectObj = projectPreferences?.map(preference => {
const matchedProjectObj = projects.find(
project => project.id === preference.links.project
)

if (matchedProjectObj) {
preference.project = matchedProjectObj
}
return preference
})
}

return (
<RecentProjects
isLoading={preferencesLoading || projectsLoading}
projectPreferences={projectPreferencesWithProjectObj}
error={preferencesError || projectsError}
/>
)
}

RecentProjectsContainer.propTypes = {
authUser: shape({
id: string
})
}
Loading