diff --git a/packages/lib-react-components/src/ProjectCard/ProjectCard.js b/packages/lib-react-components/src/ProjectCard/ProjectCard.js index 59c18a7175..1670307db4 100644 --- a/packages/lib-react-components/src/ProjectCard/ProjectCard.js +++ b/packages/lib-react-components/src/ProjectCard/ProjectCard.js @@ -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' @@ -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 ( + {badge ? + {badge} + : null} - + + + + + ) diff --git a/packages/lib-user/src/components/UserHome/components/RecentProjects/RecentProjects.js b/packages/lib-user/src/components/UserHome/components/RecentProjects/RecentProjects.js new file mode 100644 index 0000000000..107da9beb7 --- /dev/null +++ b/packages/lib-user/src/components/UserHome/components/RecentProjects/RecentProjects.js @@ -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 ( + + {isLoading && ( + + + + )} + {!isLoading && error && ( + + + There was an error fetching your recent projects + + + )} + {!isLoading && !projectPreferences.length && !error && ( + + No Recent Projects found + + Start by{' '} + + classifying any project + + . + + + )} + {!isLoading && + projectPreferences?.length ? ( + + {projectPreferences.map(preference => ( +
  • + +
  • + ))} +
    + ) : null} +
    + ) +} + +RecentProjects.propTypes = { + isLoading: bool, + projectPreferences: arrayOf( + shape({ + id: string + }) + ) +} diff --git a/packages/lib-user/src/components/UserHome/components/RecentProjects/RecentProjects.stories.js b/packages/lib-user/src/components/UserHome/components/RecentProjects/RecentProjects.stories.js new file mode 100644 index 0000000000..2757c16c89 --- /dev/null +++ b/packages/lib-user/src/components/UserHome/components/RecentProjects/RecentProjects.stories.js @@ -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` } + } +} diff --git a/packages/lib-user/src/components/UserHome/components/RecentProjects/RecentProjectsContainer.js b/packages/lib-user/src/components/UserHome/components/RecentProjects/RecentProjectsContainer.js new file mode 100644 index 0000000000..30391441a6 --- /dev/null +++ b/packages/lib-user/src/components/UserHome/components/RecentProjects/RecentProjectsContainer.js @@ -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 ( + + ) +} + +RecentProjectsContainer.propTypes = { + authUser: shape({ + id: string + }) +} diff --git a/packages/lib-user/src/components/UserHome/components/RecentSubjects/RecentSubjects.js b/packages/lib-user/src/components/UserHome/components/RecentSubjects/RecentSubjects.js index c480db4b23..e0c1747485 100644 --- a/packages/lib-user/src/components/UserHome/components/RecentSubjects/RecentSubjects.js +++ b/packages/lib-user/src/components/UserHome/components/RecentSubjects/RecentSubjects.js @@ -1,81 +1,73 @@ import { Anchor, Box, ResponsiveContext, Text } from 'grommet' import { arrayOf, bool, shape, string } from 'prop-types' import { useContext } from 'react' -import { Loader, SpacedHeading, SpacedText } from '@zooniverse/react-components' +import { Loader, SpacedText } from '@zooniverse/react-components' +import { ContentBox } from '@components/shared' import SubjectCard from '../SubjectCard/SubjectCard.js' function RecentSubjects({ isLoading = false, recents = [], - recentsError = undefined + error = undefined }) { const size = useContext(ResponsiveContext) return ( - - - Recent Classifications - - - {isLoading && ( - - - - )} - {!isLoading && recentsError && ( - - - There was an error fetching recent classifications - - - )} - {!isLoading && !recents?.length && !recentsError && ( - - No Recent Classifications found - - Start by{' '} - - classifying any project - {' '} - to show your recent classifications here. - - - )} - {!isLoading && recents?.length - ? recents.slice(0, 10).map(classification => { - const subjectMedia = classification.locations.map( + + {isLoading && ( + + + + )} + {!isLoading && error && ( + + + There was an error fetching recent classifications + + + )} + {!isLoading && !recents?.length && !error && ( + + No Recent Classifications found + + Start by{' '} + + classifying any project + {' '} + to show your recent classifications here. + + + )} + {!isLoading && recents?.length + ? ( + + {recents.map(recent => { + const subjectMedia = recent?.locations?.map( location => Object.values(location)[0] ) return ( - +
  • + +
  • ) - }) - : null} -
    -
    + })} +
    + ) : null} + ) } diff --git a/packages/lib-user/src/components/UserHome/components/RecentSubjects/RecentSubjects.stories.js b/packages/lib-user/src/components/UserHome/components/RecentSubjects/RecentSubjects.stories.js index 919dffafa8..8a0d4abd0c 100644 --- a/packages/lib-user/src/components/UserHome/components/RecentSubjects/RecentSubjects.stories.js +++ b/packages/lib-user/src/components/UserHome/components/RecentSubjects/RecentSubjects.stories.js @@ -21,6 +21,6 @@ export const NoSubjects = { export const Error = { args: { recents: [], - recentsError: { message: `Couldn't fetch recent classifications` } + error: { message: `Couldn't fetch recent classifications` } } } diff --git a/packages/lib-user/src/components/UserHome/components/RecentSubjects/RecentSubjectsContainer.js b/packages/lib-user/src/components/UserHome/components/RecentSubjects/RecentSubjectsContainer.js index 1cef223225..efb0e06762 100644 --- a/packages/lib-user/src/components/UserHome/components/RecentSubjects/RecentSubjectsContainer.js +++ b/packages/lib-user/src/components/UserHome/components/RecentSubjects/RecentSubjectsContainer.js @@ -1,7 +1,8 @@ -import useSWR from 'swr' import { shape, string } from 'prop-types' +import { panoptes } from '@zooniverse/panoptes-js' +import useSWR from 'swr' -import fetchRecentSubjects from './fetchRecentSubjects.js' +import { usePanoptesProjects } from '@hooks' import RecentSubjects from './RecentSubjects.js' const SWROptions = { @@ -12,19 +13,50 @@ const SWROptions = { refreshInterval: 0 } +async function fetchUserRecents({ userId }) { + try { + const query = { + page_size: 10, + sort: '-created_at' + } + const response = await panoptes.get(`/users/${userId}/recents`, query) + return response.body?.recents + } catch (error) { + console.error(error) + throw error + } +} + function RecentSubjectsContainer({ authUser }) { const cacheKey = { name: 'user-recent-classifications', userId: authUser.id } - const { - data: recents, - error: recentsError, - isLoading - } = useSWR(cacheKey, fetchRecentSubjects, SWROptions) + const { data: recents, isLoading: recentsLoading, error: recentsError } = useSWR(cacheKey, fetchUserRecents, SWROptions) + + const recentProjectIds = [...new Set(recents?.map(recent => recent.links?.project))] + + const { data: projects, isLoading: projectsLoading, error: projectsError } = usePanoptesProjects(recentProjectIds) + + // Attach project slug to each recent + let recentsWithProjectSlugs + if (projects?.length) { + recentsWithProjectSlugs = recents.map(recent => { + const { slug } = projects.find(project => recent.links.project === project.id) + recent.project_slug = slug + return recent + }) + } + + const error = recentsError || projectsError + const isLoading = recentsLoading || projectsLoading return ( - + ) } diff --git a/packages/lib-user/src/components/UserHome/components/RecentSubjects/fetchRecentSubjects.js b/packages/lib-user/src/components/UserHome/components/RecentSubjects/fetchRecentSubjects.js deleted file mode 100644 index bb6058b92c..0000000000 --- a/packages/lib-user/src/components/UserHome/components/RecentSubjects/fetchRecentSubjects.js +++ /dev/null @@ -1,57 +0,0 @@ -import { panoptes } from '@zooniverse/panoptes-js' -import auth from 'panoptes-client/lib/auth' - -async function fetchUserRecents() { - try { - const user = await auth.checkCurrent() - const token = await auth.checkBearerToken() - const authorization = `Bearer ${token}` - const query = { - page: 1, - sort: '-created_at' - } - const response = await panoptes.get(`/users/${user.id}/recents`, query, { - authorization - }) - return response.body?.recents - } catch (error) { - console.error(error) - return [] - } -} - -/* Anatomy of a classification object */ -// [ -// { -// id: "258337208", -// links: { -// project: "19072", -// workflow: "22152", -// subject: "80286011" -// }, -// ... -// }, -// ... -// ] - -async function fetchSubjectLinks(classification) { - try { - const response = await panoptes.get('/projects', { - id: classification.links?.project - }) - const slug = response.body?.projects[0].slug - classification.slug = slug - } catch { - console.error(error) - } -} - -export default async function fetchRecentSubjects() { - const recents = await fetchUserRecents() - - if (recents?.length) { - await Promise.allSettled(recents.map(fetchSubjectLinks)) - } - - return recents -} diff --git a/packages/lib-user/src/components/UserHome/components/SubjectCard/SubjectCard.js b/packages/lib-user/src/components/UserHome/components/SubjectCard/SubjectCard.js index 4fbac524a5..55f1675675 100644 --- a/packages/lib-user/src/components/UserHome/components/SubjectCard/SubjectCard.js +++ b/packages/lib-user/src/components/UserHome/components/SubjectCard/SubjectCard.js @@ -14,17 +14,10 @@ import { Anchor, Box } from 'grommet' import { Media, SpacedText } from '@zooniverse/react-components' import { string } from 'prop-types' -const StyledAnchor = styled(Anchor)` - text-decoration: none; - - &:hover { - text-decoration: none; - } -` - const StyledBox = styled(Box)` overflow: hidden; position: relative; + text-decoration: none; ` const Gradient = styled(Box)` @@ -73,25 +66,25 @@ export default function SubjectCard({ const href = `https://www.zooniverse.org/projects/${projectSlug}/talk/subjects/${subjectID}` return ( - - - - - - {'Subject ' + subjectID} - - - + + + + + {'Subject ' + subjectID} + + ) } diff --git a/packages/lib-user/src/components/shared/TopProjects/TopProjects.js b/packages/lib-user/src/components/shared/TopProjects/TopProjects.js index 08a85292ab..14370a1331 100644 --- a/packages/lib-user/src/components/shared/TopProjects/TopProjects.js +++ b/packages/lib-user/src/components/shared/TopProjects/TopProjects.js @@ -30,7 +30,7 @@ function CardsRow({ children }) { as='ul' direction='row' gap='small' - pad={{ horizontal: 'xxsmall', bottom: 'xsmall' }} + pad={{ horizontal: 'xxsmall', bottom: 'xsmall', top: 'xxsmall' }} overflow={{ horizontal: 'auto' }} style={{ listStyle: 'none' }} >