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

app-root, lib-user: Add ProfileHeader to UserStats, refactor related API requests #5898

Merged
merged 14 commits into from
Feb 21, 2024
Merged
6 changes: 4 additions & 2 deletions packages/app-root/src/app/users/[login]/stats/page.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
'use client'

import { UserStats } from '@zooniverse/user'
import auth from 'panoptes-client/lib/auth'

export default function UserPage() {
return (
<UserStats />
<UserStats
authClient={auth}
/>
)
}
8 changes: 4 additions & 4 deletions packages/lib-user/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
57 changes: 31 additions & 26 deletions packages/lib-user/src/components/UserStats/UserStats.js
Original file line number Diff line number Diff line change
@@ -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 (
<Layout>
<div>
<div style={{
borderRadius: '8px',
border: '0.5px solid #A6A7A9',
boxShadow: '0px 1px 4px 0px rgba(0, 0, 0, 0.25)',
color: 'black',
height: '472px',
marginBottom: '30px'
}}>
<p>User profile header goes here.</p>
<p>Bar chart goes here.</p>
</div>
<div style={{
borderRadius: '8px',
border: '0.5px solid #A6A7A9',
boxShadow: '0px 1px 4px 0px rgba(0, 0, 0, 0.25)',
color: 'black',
height: '300px'
}}>
<p>Top projects goes here.</p>
</div>
</div>
<ContentBox
direction='column'
gap='32px'
height='400px'
>
<ProfileHeader
avatar={user?.avatar_src}
classifications={userStats?.total_count}
displayName={user?.display_name}
login={user?.login}
projects={userStats?.project_contributions?.length}
/>
</ContentBox>
</Layout>
)
}

UserStats.propTypes = {
children: node
authClient: object
}

export default UserStats
18 changes: 10 additions & 8 deletions packages/lib-user/src/components/shared/BarChart/BarChart.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -104,8 +107,7 @@ BarChart.propTypes = {
session_time: number
})),
dateRange: string,
screenSize: string,
type: string
}

export default withResponsiveContext(BarChart)
export default BarChart
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Box } from 'grommet'

import dateRanges from './helpers/dateRanges.js'
import {
dateRanges
} from '@utils'

import {
last7days,
last30days,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
1 change: 1 addition & 0 deletions packages/lib-user/src/components/shared/Layout/Layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ function Layout ({ children }) {
dark: 'dark-3',
light: 'neutral-6'
}}
gap='32px'
>
{children}
</PageBody>
Expand Down
1 change: 1 addition & 0 deletions packages/lib-user/src/components/shared/Layout/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as Layout } from './Layout.js'
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,17 @@ function TitledStat ({
title = '',
value = 0
}) {
let displayValue = value
if (isNaN(value)) {
displayValue = 0
}
Comment on lines +20 to +22
Copy link
Contributor

Choose a reason for hiding this comment

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

In what scenario could this be NaN? I ask because the value prop passed to TitledStat is a required number, so without an inline code comment here I'm confused why these additional lines are needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, great question, the reason these lines I think are necessary surprised me. typeof NaN is actually 'number' (which made me lol, I had forgotten that JavaScript quirk). NaN can result from invalid arithmetic operations (as well as other reasons), and I think while loading stats or if there are no hours then the result of the equation that converts the seconds can result in NaN.


return (
<Box
align='center'
>
<SpacedText
color={{ dark: 'neutral-6', light: 'neutral-7' }}
size='xsmall'
uppercase={false}
>
{title}
Expand All @@ -32,7 +36,7 @@ function TitledStat ({
size='xlarge'
weight='bold'
>
{value}
{Math.round(displayValue).toLocaleString()}
</SpacedText>
</Box>
)
Expand All @@ -45,12 +49,12 @@ TitledStat.propTypes = {

function ProfileHeader ({
avatar = '',
classifications = 0,
contributors = 0,
classifications = undefined,
mcbouslog marked this conversation as resolved.
Show resolved Hide resolved
contributors = undefined,
displayName = '',
hours = 0,
hours = undefined,
login = '',
projects = 0,
projects = undefined,
screenSize = 'medium'
mcbouslog marked this conversation as resolved.
Show resolved Hide resolved
}) {
return (
Expand Down Expand Up @@ -97,25 +101,25 @@ function ProfileHeader ({
direction='row'
gap='small'
>
{classifications ?
{classifications !== undefined ?
<TitledStat
title='Classifications'
value={classifications}
/>
: null}
{hours ?
{hours !== undefined ?
<TitledStat
title='Hours'
value={hours}
/>
: null}
{contributors ?
{contributors !== undefined ?
<TitledStat
title='Contributors'
value={contributors}
/>
: null}
{projects ?
{projects !== undefined ?
<TitledStat
title='Projects'
value={projects}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ describe('components > shared > ProfileHeader', function () {

it('should show the group\'s classifications', function () {
render(<GroupStory />)
const classifications = screen.getByText('1526')
const classifications = screen.getByText('1,526')
expect(classifications).to.be.ok()
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
11 changes: 5 additions & 6 deletions packages/lib-user/src/hooks/usePanoptesAuth.js
Original file line number Diff line number Diff line change
@@ -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])

Expand Down
34 changes: 11 additions & 23 deletions packages/lib-user/src/hooks/usePanoptesUser.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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)
Expand All @@ -45,7 +33,7 @@ export default function usePanoptesUser(authClient) {
authClient.listen('change', checkUserSession)

return function () {
authClient?.stopListening('change', checkUserSession)
authClient.stopListening('change', checkUserSession)
}
}, [authClient])

Expand Down
Loading