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 4 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
50 changes: 33 additions & 17 deletions packages/lib-react-components/src/ProjectCard/ProjectCard.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Box } from 'grommet'
import { Box, Text } from 'grommet'
import { 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>}
</Box>
<StyledProjectContent
flex='grow'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export default {

export const NfnCaliFlowers = {
args: {
classifications: 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 All @@ -25,6 +26,7 @@ export const NfnCaliFlowers = {

export const PlanetHuntersTess = {
args: {
classifications: 9564,
description: 'Join the Search for Undiscovered Worlds',
displayName: 'Planet Hunters TESS',
imageSrc: 'https://panoptes-uploads.zooniverse.org/project_avatar/442e8392-6c46-4481-8ba3-11c6613fba56.jpeg',
Expand Down
15 changes: 12 additions & 3 deletions packages/lib-user/src/components/UserHome/UserHome.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
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}/>
<RecentSubjectsContainer 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
Expand Up @@ -28,30 +28,37 @@ export default function Dashboard({ isLoading = false, user }) {
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' }
}
/>
</Relative>
background={
isLoading || !user?.profile_header
? 'brand'
: { image: `url(${user.profile_header})` }
}
round={size !== 'small' ? { size: '16px', corner: 'top' } : false}
>
<StyledAvatar
alt='User avatar'
src={
!user?.avatar_src || isLoading
? 'https://www.zooniverse.org/assets/simple-avatar.png'
: user.avatar_src
}
/>
</Relative>
</Box>
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
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,
projects = [],
error = undefined
}) {
const size = useContext(ResponsiveContext)

return (
<ContentBox title='Continue Classifying' screenSize={size}>
<Box
as='ul'
direction='row'
gap='small'
pad={{ horizontal: 'xxsmall', bottom: 'xsmall' }}
overflow={{ horizontal: 'auto' }}
style={{ listStyle: 'none' }}
margin='0'
>
{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
</SpacedText>
</Box>
)}
{!isLoading && !projects.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 &&
projects?.length &&
projects.map(project => (
<ProjectCard
key={project?.id}
badge={project?.user_classifications}
description={project?.description}
displayName={project?.display_name}
href={`https://www.zooniverse.org/projects/${project?.slug}`}
imageSrc={project?.avatar_src}
size={size}
/>
))}
</Box>
</ContentBox>
)
}

RecentProjects.propTypes = {
isLoading: bool,
projects: 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 projectsWithCount = PROJECTS.map(project => {
project.user_classifications = Math.floor(Math.random() * 100)
return project
})

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

export const Default = {
args: {
projects: projectsWithCount
}
}

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

export const Error = {
args: {
projects: [],
error: { message: `Couldn't fetch recent projects` }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { shape, string } from 'prop-types'

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

export default function RecentProjectsContainer({ authUser }) {
// Returns all projects a user has classified in descending order (most classifications first)
const { data: statsData } = useStats({
sourceId: authUser.id,
query: { project_contributions: true }
})
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mcbouslog @seanmiller26 as mentioned in the Slack thread, the response from this is a list of projects in descending order from greatest number of classifications to least number of classifications by the user.

Is this "Continue Classifying" section intended to be "projects you've most recently classified"? If so, I'll need to change this fetch to a user's project preferences like PFE's homepage.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, this is a recently classified section, so we can match PFE here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good - I refactored to display projects a user has most recently classified on. The catch with relying on project_preferences is that project_preferences.updated_at is changed when a user interacts with a project beyond just classifying. For instance, closing a tutorial.

In this PR, I only fetched 1 page of project_preferences (20 items) and filtered for those where activity_count > 0 (which means a user has actually classified on a project). This means if I recently interacted with 20 projects, but only classified on 3, then only 3 projects will show in this "Continue Classifying" UI section.

It would be much smoother if the eras request for project_contributions included a sort by most recently classified, but I'll have to follow-up with Michelle after internal team testing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A clarification - PFE's homepage recursively gets ALL pages of a user's project preferences rather than just 1 page. This is why it takes a long time for all of your ribbon classifications to load. I'd prefer not to use recursion on the new homepage because it's so data intensive - hence the note about follow-up with Michelle and eras.

Copy link
Contributor

Choose a reason for hiding this comment

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

I did not know the specifics here re project_preferences.updated_at Great decisions!


// We only display 10 in the UI, so only fetch project data for the first 10 statsData
const recentProjectIds = statsData?.project_contributions
?.slice(0, 10)
.map(project => project.project_id)

const { data: projects } = usePanoptesProjects(recentProjectIds)

let projectsWithClassificationCount

// Attach 'count' to each project object
if (projects?.length) {
projectsWithClassificationCount = projects?.map(project => {
const { count } = statsData?.project_contributions?.find(
stat => stat.project_id.toString() === project.id
)
project.user_classifications = count
return project
})
// Re-sort into descending order (most classifications first)
projectsWithClassificationCount.sort(
(a, b) => b.user_classifications - a.user_classifications
)
}

return <RecentProjects projects={projectsWithClassificationCount} />
}

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