Skip to content

Commit

Permalink
Homepage: Create RecentProjects component, refactor data fetching in …
Browse files Browse the repository at this point in the history
…RecentSubjects (#6125)

* fetch and display project cards for 'project_contributions' on hompage

* add count badge

* refactor RecentSubjects to fetch data like RecentProjects

* create grid layout in UserHome

* remove edits to Dashboard

* use project_preferences for most recently classified projects

* minor fix for new users with no projectPreferences

* Refactor RecentProjects and RecentSubject containers to throw fetch error

* Refactor Recent projects and subjects components ul and li's

* Refactor Recent projects and subjects stories

* Refactor project and subject cards for improved focus styling

* Refactor recents in RecentSubjectsContainer

* Add await for checkCurrent user

---------

Co-authored-by: Mark Bouslog <[email protected]>
  • Loading branch information
goplayoutside3 and mcbouslog authored Jun 14, 2024
1 parent 1a9e6c2 commit 37bef25
Show file tree
Hide file tree
Showing 12 changed files with 362 additions and 175 deletions.
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 />
</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
</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'
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}>
<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 = await 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
}
}

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

0 comments on commit 37bef25

Please sign in to comment.