diff --git a/packages/app-root/src/app/users/[login]/stats/page.js b/packages/app-root/src/app/users/[login]/stats/page.js index 16a6212f93..827a4c378d 100644 --- a/packages/app-root/src/app/users/[login]/stats/page.js +++ b/packages/app-root/src/app/users/[login]/stats/page.js @@ -1,9 +1,11 @@ 'use client' - import { UserStats } from '@zooniverse/user' +import auth from 'panoptes-client/lib/auth' export default function UserPage() { return ( - + ) } diff --git a/packages/lib-user/package.json b/packages/lib-user/package.json index 2ba39682d1..270f6e1668 100644 --- a/packages/lib-user/package.json +++ b/packages/lib-user/package.json @@ -32,11 +32,12 @@ "build-storybook": "storybook build" }, "dependencies": { + "@zooniverse/panoptes-js": "~0.4.0", + "panoptes-client": "~5.6.0", "swr": "~2.2.4" }, "peerDependencies": { "@zooniverse/grommet-theme": "3.x.x", - "@zooniverse/panoptes-js": "~0.4.0", "@zooniverse/react-components": "~1.x.x", "grommet": "2.x.x", "react": "~18.2.0", @@ -70,9 +71,8 @@ "html-webpack-plugin": "~5.6.0", "ignore-styles": "~5.0.1", "jsdom": "~24.0.0", - "mocha": "~10.3.0", - "nock": "~13.5.1", - "panoptes-client": "~5.6.0", + "mocha": "~10.2.0", + "nock": "~13.4.0", "process": "~0.11.10", "prop-types": "^15.8.1", "sinon": "~17.0.0", diff --git a/packages/lib-user/src/components/UserStats/UserStats.js b/packages/lib-user/src/components/UserStats/UserStats.js index 5f251e4669..a233c7a997 100644 --- a/packages/lib-user/src/components/UserStats/UserStats.js +++ b/packages/lib-user/src/components/UserStats/UserStats.js @@ -1,44 +1,49 @@ 'use client' -// This component is a work in progress. It is not intended to be imported as-is, but is currently being used for initial UserStats local development. +import { object } from 'prop-types' -import { node } from 'prop-types' +import { + usePanoptesUser, + useUserStats +} from '@hooks' import Layout from '../shared/Layout/Layout' +import ContentBox from '../shared/ContentBox/ContentBox' +import ProfileHeader from '../shared/ProfileHeader/ProfileHeader' function UserStats ({ - children + authClient }) { + const { data: user, error, isLoading } = usePanoptesUser(authClient) + + const statsQuery = { + period: 'year', + project_contributions: true, + time_spent: true + } + const { data: userStats, error: statsError, isLoading: statsLoading } = useUserStats({ authClient, query: statsQuery, userID: user?.id }) + return ( - - - User profile header goes here. - Bar chart goes here. - - - Top projects goes here. - - + + + ) } UserStats.propTypes = { - children: node + authClient: object } export default UserStats diff --git a/packages/lib-user/src/components/shared/BarChart/BarChart.js b/packages/lib-user/src/components/shared/BarChart/BarChart.js index 00cba54167..be6daddb66 100644 --- a/packages/lib-user/src/components/shared/BarChart/BarChart.js +++ b/packages/lib-user/src/components/shared/BarChart/BarChart.js @@ -1,16 +1,19 @@ -import { DataChart, Text } from 'grommet' +import { DataChart, ResponsiveContext, Text } from 'grommet' import { arrayOf, number, shape, string } from 'prop-types' -import withResponsiveContext from '@zooniverse/react-components/helpers/withResponsiveContext' +import { useContext } from 'react' + +import { + dateRanges +} from '@utils' -import dateRanges from './helpers/dateRanges' import getDateRangeLabel from './helpers/getDateRangeLabel' function BarChart ({ data = [], dateRange = dateRanges.Last7Days, - screenSize = 'small', type = 'count' }) { + const size = useContext(ResponsiveContext) const dateRangeLabel = getDateRangeLabel(dateRange) const typeLabel = type === 'count' ? 'Classifications' : 'Time' const readableDateRange = dateRange @@ -24,10 +27,10 @@ function BarChart ({ property: type, type: 'bar' } - if (screenSize !== 'small' && data.length < 9) { + if (size !== 'small' && data.length < 9) { chartOptions.thickness = 'xlarge' } - if (screenSize === 'small') { + if (size === 'small') { if (data.length < 12) { chartOptions.thickness = 'small' } else if (data.length > 11 && data.length < 19) { @@ -104,8 +107,7 @@ BarChart.propTypes = { session_time: number })), dateRange: string, - screenSize: string, type: string } -export default withResponsiveContext(BarChart) +export default BarChart diff --git a/packages/lib-user/src/components/shared/BarChart/BarChart.stories.js b/packages/lib-user/src/components/shared/BarChart/BarChart.stories.js index d6cf4a7ab1..b475140837 100644 --- a/packages/lib-user/src/components/shared/BarChart/BarChart.stories.js +++ b/packages/lib-user/src/components/shared/BarChart/BarChart.stories.js @@ -1,6 +1,9 @@ import { Box } from 'grommet' -import dateRanges from './helpers/dateRanges.js' +import { + dateRanges +} from '@utils' + import { last7days, last30days, diff --git a/packages/lib-user/src/components/shared/ContentBox/ContentBox.js b/packages/lib-user/src/components/shared/ContentBox/ContentBox.js index afbd105655..ad009bbd62 100644 --- a/packages/lib-user/src/components/shared/ContentBox/ContentBox.js +++ b/packages/lib-user/src/components/shared/ContentBox/ContentBox.js @@ -36,7 +36,7 @@ function ContentBox({ }} border={border} elevation={screenSize === 'small' ? 'none' : 'xsmall'} - margin={screenSize === 'small' ? 'none' : '30px'} + margin={screenSize === 'small' ? '30px' : 'none'} pad='30px' round={screenSize === 'small' ? 'none' : '8px'} {...rest} diff --git a/packages/lib-user/src/components/shared/Layout/Layout.js b/packages/lib-user/src/components/shared/Layout/Layout.js index 5cd374cc5e..2631d0aa5e 100644 --- a/packages/lib-user/src/components/shared/Layout/Layout.js +++ b/packages/lib-user/src/components/shared/Layout/Layout.js @@ -128,6 +128,7 @@ function Layout ({ children }) { dark: 'dark-3', light: 'neutral-6' }} + gap='32px' > {children} diff --git a/packages/lib-user/src/components/shared/Layout/index.js b/packages/lib-user/src/components/shared/Layout/index.js new file mode 100644 index 0000000000..768acd5680 --- /dev/null +++ b/packages/lib-user/src/components/shared/Layout/index.js @@ -0,0 +1 @@ +export { default as Layout } from './Layout.js' diff --git a/packages/lib-user/src/components/shared/ProfileHeader/ProfileHeader.js b/packages/lib-user/src/components/shared/ProfileHeader/ProfileHeader.js index cf4f7e44f6..837a40e581 100644 --- a/packages/lib-user/src/components/shared/ProfileHeader/ProfileHeader.js +++ b/packages/lib-user/src/components/shared/ProfileHeader/ProfileHeader.js @@ -16,13 +16,17 @@ function TitledStat ({ title = '', value = 0 }) { + let displayValue = value + if (isNaN(value)) { + displayValue = 0 + } + return ( {title} @@ -32,7 +36,7 @@ function TitledStat ({ size='xlarge' weight='bold' > - {value} + {Math.round(displayValue).toLocaleString()} ) @@ -45,12 +49,12 @@ TitledStat.propTypes = { function ProfileHeader ({ avatar = '', - classifications = 0, - contributors = 0, + classifications = undefined, + contributors = undefined, displayName = '', - hours = 0, + hours = undefined, login = '', - projects = 0, + projects = undefined, screenSize = 'medium' }) { return ( @@ -97,25 +101,25 @@ function ProfileHeader ({ direction='row' gap='small' > - {classifications ? + {classifications !== undefined ? : null} - {hours ? + {hours !== undefined ? : null} - {contributors ? + {contributors !== undefined ? : null} - {projects ? + {projects !== undefined ? shared > ProfileHeader', function () { it('should show the group\'s classifications', function () { render() - const classifications = screen.getByText('1526') + const classifications = screen.getByText('1,526') expect(classifications).to.be.ok() }) diff --git a/packages/lib-user/src/components/shared/Select/Select.spec.js b/packages/lib-user/src/components/shared/Select/Select.spec.js index 6776a7d630..3e93696826 100644 --- a/packages/lib-user/src/components/shared/Select/Select.spec.js +++ b/packages/lib-user/src/components/shared/Select/Select.spec.js @@ -2,7 +2,10 @@ import { composeStory } from '@storybook/react' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import dateRanges from '../BarChart/helpers/dateRanges' +import { + dateRanges +} from '@utils' + import Meta, { DateRanges } from './Select.stories' describe('components > shared > Select', function() { diff --git a/packages/lib-user/src/components/shared/Select/Select.stories.js b/packages/lib-user/src/components/shared/Select/Select.stories.js index e8af2542ad..b23dce43ad 100644 --- a/packages/lib-user/src/components/shared/Select/Select.stories.js +++ b/packages/lib-user/src/components/shared/Select/Select.stories.js @@ -1,7 +1,10 @@ import { Box } from 'grommet' + import Select from './Select.js' -import dateRanges from '../BarChart/helpers/dateRanges.js' +import { + dateRanges +} from '@utils' export default { title: 'Components/shared/Select', diff --git a/packages/lib-user/src/hooks/usePanoptesAuth.js b/packages/lib-user/src/hooks/usePanoptesAuth.js index 369c15d2f4..52ab0ba889 100644 --- a/packages/lib-user/src/hooks/usePanoptesAuth.js +++ b/packages/lib-user/src/hooks/usePanoptesAuth.js @@ -1,16 +1,15 @@ import { useEffect, useState } from 'react' -import { getBearerToken } from '@utils/index.js' +import { getBearerToken } from '@utils' export default function usePanoptesAuth({ authClient, userID }) { const [authorization, setAuthorization] = useState() + async function checkAuth() { + const token = await getBearerToken(authClient) + setAuthorization(token) + } useEffect(function onUserChange() { - async function checkAuth() { - const token = await getBearerToken(authClient) - setAuthorization(token) - } - checkAuth() }, [authClient, userID]) diff --git a/packages/lib-user/src/hooks/usePanoptesUser.js b/packages/lib-user/src/hooks/usePanoptesUser.js index 507ef03b3a..6a017c53d0 100644 --- a/packages/lib-user/src/hooks/usePanoptesUser.js +++ b/packages/lib-user/src/hooks/usePanoptesUser.js @@ -1,27 +1,15 @@ -import { auth } from '@zooniverse/panoptes-js' +import auth from 'panoptes-client/lib/auth' import { useEffect, useState } from 'react' -import { getBearerToken } from '@utils/index.js' - -// TODO: refactor with SWR - -async function fetchPanoptesUser(authClient) { - try { - const authorization = await getBearerToken(authClient) - if (authorization) { - const { user, error } = await auth.decodeJWT(authorization) - if (user) { - return user - } - if (error) { - throw error - } - } - return await authClient.checkCurrent() - } catch (error) { - console.log(error) - return null +async function fetchPanoptesUser() { + const panoptesUser = await auth.checkCurrent() + if (panoptesUser) { + // A lot of user properties are not needed in lib-user, so we only return the ones we need; edit as needed. + const { admin, avatar_src, display_name, id, login } = panoptesUser + return { admin, avatar_src, display_name, id, login } } + + return null } export default function usePanoptesUser(authClient) { @@ -33,7 +21,7 @@ export default function usePanoptesUser(authClient) { async function checkUserSession() { setLoading(true) try { - const panoptesUser = await fetchPanoptesUser(authClient) + const panoptesUser = await fetchPanoptesUser() setUser(panoptesUser) } catch (error) { setError(error) @@ -45,7 +33,7 @@ export default function usePanoptesUser(authClient) { authClient.listen('change', checkUserSession) return function () { - authClient?.stopListening('change', checkUserSession) + authClient.stopListening('change', checkUserSession) } }, [authClient]) diff --git a/packages/lib-user/src/hooks/useUserStats.js b/packages/lib-user/src/hooks/useUserStats.js index babfa8d803..6181b96e24 100644 --- a/packages/lib-user/src/hooks/useUserStats.js +++ b/packages/lib-user/src/hooks/useUserStats.js @@ -1,42 +1,46 @@ -import { useEffect, useState } from 'react' +import { env } from '@zooniverse/panoptes-js' +import useSWR from 'swr' -import { getBearerToken } from '@utils/index.js' +import { usePanoptesAuth } from '@hooks' -// TODO: refactor with SWR +const SWROptions = { + revalidateIfStale: true, + revalidateOnMount: true, + revalidateOnFocus: true, + revalidateOnReconnect: true, + refreshInterval: 0 +} + +function statsHost(env) { + switch (env) { + case 'production': + return 'https://eras.zooniverse.org' + default: + return 'https://eras-staging.zooniverse.org' + } +} -export default function useUserStats({ authClient, userID }) { - const [error, setError] = useState(null) - const [userStats, setUserStats] = useState(null) - const [loading, setLoading] = useState(true) +const defaultEndpoint = '/classifications/users' - useEffect(function () { - async function fetchUserStats() { - setLoading(true) - setUserStats(null) - - try { - const authorization = await getBearerToken(authClient) - const headers = { authorization } - const response = await fetch(`https://eras-staging.zooniverse.org/classifications/users/${userID}?period=week`, { headers }) - const data = await response.json() - if (!ignore) { - setUserStats(data) - } - } catch (error) { - setError(error) - } - - setLoading(false) - } +async function fetchUserStats({ endpoint, query, userID, authorization }) { + const stats = statsHost(env) + const queryParams = new URLSearchParams(query).toString() + const headers = { authorization } + + try { + const response = await fetch(`${stats}${endpoint}/${userID}?${queryParams}`, { headers }) + const data = await response.json() + return data + } catch (error) { + console.log(error) + return null + } +} - let ignore = false - if (authClient && userID) { - fetchUserStats(authClient, userID) - } - return () => { - ignore = true - } - }, [authClient, userID]) +export default function useUserStats({ authClient, endpoint = defaultEndpoint, query, userID }) { + const authorization = usePanoptesAuth({ authClient, userID }) + + const key = (authorization && userID) ? { endpoint, query, userID, authorization } : null - return { data: userStats, error, isLoading: loading } + return useSWR(key, fetchUserStats, SWROptions) } diff --git a/packages/lib-user/src/index.js b/packages/lib-user/src/index.js index 24f7c6a2e9..75a1891efb 100644 --- a/packages/lib-user/src/index.js +++ b/packages/lib-user/src/index.js @@ -13,6 +13,7 @@ export { default as useUserStats } from './hooks/useUserStats.js' // utils export { default as createPanoptesUserGroup } from './utils/createPanoptesUserGroup.js' +export { default as dateRanges } from './utils/dateRanges.js' export { default as deletePanoptesUserGroup } from './utils/deletePanoptesUserGroup.js' export { default as getBearerToken } from './utils/getBearerToken.js' export { default as updatePanoptesUserGroup} from './utils/updatePanoptesUserGroup.js' diff --git a/packages/lib-user/src/components/shared/BarChart/helpers/dateRanges.js b/packages/lib-user/src/utils/dateRanges.js similarity index 100% rename from packages/lib-user/src/components/shared/BarChart/helpers/dateRanges.js rename to packages/lib-user/src/utils/dateRanges.js diff --git a/packages/lib-user/src/components/shared/BarChart/helpers/dateRanges.spec.js b/packages/lib-user/src/utils/dateRanges.spec.js similarity index 100% rename from packages/lib-user/src/components/shared/BarChart/helpers/dateRanges.spec.js rename to packages/lib-user/src/utils/dateRanges.spec.js diff --git a/packages/lib-user/src/utils/index.js b/packages/lib-user/src/utils/index.js index b44ec40324..4ac053f88e 100644 --- a/packages/lib-user/src/utils/index.js +++ b/packages/lib-user/src/utils/index.js @@ -1,4 +1,5 @@ export { default as createPanoptesUserGroup } from './createPanoptesUserGroup.js' +export { default as dateRanges } from './dateRanges.js' export { default as deletePanoptesUserGroup } from './deletePanoptesUserGroup.js' export { default as getBearerToken } from './getBearerToken.js' export { default as updatePanoptesUserGroup } from './updatePanoptesUserGroup.js' \ No newline at end of file diff --git a/packages/lib-user/webpack.dev.js b/packages/lib-user/webpack.dev.js index 87e86f0d23..3d98a4281a 100644 --- a/packages/lib-user/webpack.dev.js +++ b/packages/lib-user/webpack.dev.js @@ -49,6 +49,7 @@ module.exports = { fs: false, // for markdown-it plugins path: require.resolve("path-browserify"), + process: false, url: false, } },
User profile header goes here.
Bar chart goes here.
Top projects goes here.