From 4e1f716f27b706523d6ff4b81a0ac32921714dfd Mon Sep 17 00:00:00 2001
From: Delilah <23665803+goplayoutside3@users.noreply.github.com>
Date: Fri, 15 Nov 2024 12:19:57 -0600
Subject: [PATCH 01/16] create new YourProjectStats component with useSWR for
data
---
.../src/screens/ClassifyPage/ClassifyPage.js | 3 +-
.../YourProjectStats/YourProjectStats.js | 47 +++++++
.../YourProjectStats/YourProjectStats.spec.js | 0
.../YourProjectStats.stories.js | 8 ++
.../YourProjectStats/useYourProjectStats.js | 119 ++++++++++++++++++
.../src/shared/components/Stat/Stat.js | 26 ++--
.../Dashboard/DashboardContainer.js | 1 +
7 files changed, 195 insertions(+), 9 deletions(-)
create mode 100644 packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStats.js
create mode 100644 packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStats.spec.js
create mode 100644 packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStats.stories.js
create mode 100644 packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/useYourProjectStats.js
diff --git a/packages/app-project/src/screens/ClassifyPage/ClassifyPage.js b/packages/app-project/src/screens/ClassifyPage/ClassifyPage.js
index e1b084dae2..c9443d0b41 100644
--- a/packages/app-project/src/screens/ClassifyPage/ClassifyPage.js
+++ b/packages/app-project/src/screens/ClassifyPage/ClassifyPage.js
@@ -10,6 +10,7 @@ import ProjectStatistics from '@shared/components/ProjectStatistics'
import FinishedForTheDay from './components/FinishedForTheDay'
import RecentSubjects from './components/RecentSubjects'
import YourStats from './components/YourStats'
+import YourProjectStats from './components/YourProjectStats/YourProjectStats.js'
import StandardLayout from '@shared/components/StandardLayout'
import WorkflowAssignmentModal from './components/WorkflowAssignmentModal'
import WorkflowMenuModal from './components/WorkflowMenuModal'
@@ -115,7 +116,7 @@ function ClassifyPage({
columns={screenSize === 'small' ? ['auto'] : ['1fr', '2fr']}
gap={screenSize === 'small' ? 'small' : 'medium'}
>
-
+
diff --git a/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStats.js b/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStats.js
new file mode 100644
index 0000000000..b04edbafa3
--- /dev/null
+++ b/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStats.js
@@ -0,0 +1,47 @@
+import { Box } from 'grommet'
+import { useContext } from 'react'
+import { MobXProviderContext, observer } from 'mobx-react'
+
+import Stat from '@shared/components/Stat'
+import useYourProjectStats from './useYourProjectStats.js'
+
+function storeMapper(store) {
+ const { project, user } = store
+
+ return {
+ projectID: project.id,
+ userID: user.id
+ }
+}
+
+function YourProjectStats() {
+ const { store } = useContext(MobXProviderContext)
+ const { projectID, userID } = storeMapper(store)
+
+ const { data, loading, error } = useYourProjectStats({ projectID, userID })
+ // console.log(data)
+
+ return (
+ <>
+ {userID ? (
+ <>
+ {error ? (
+ There was an error loading your stats
+ ) : (
+
+
+
+
+ )}
+ >
+ ) : (
+
+
+
+
+ )}
+ >
+ )
+}
+
+export default observer(YourProjectStats)
diff --git a/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStats.spec.js b/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStats.spec.js
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStats.stories.js b/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStats.stories.js
new file mode 100644
index 0000000000..50dade6109
--- /dev/null
+++ b/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStats.stories.js
@@ -0,0 +1,8 @@
+import YourProjectStats from './YourProjectStats'
+
+export default {
+ title: 'Classify / YourProjectStats',
+ component: YourProjectStats,
+}
+
+export const Default = {}
diff --git a/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/useYourProjectStats.js b/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/useYourProjectStats.js
new file mode 100644
index 0000000000..6dd79fcd7b
--- /dev/null
+++ b/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/useYourProjectStats.js
@@ -0,0 +1,119 @@
+import useSWR from 'swr'
+import { usePanoptesAuth } from '@hooks'
+import { env, panoptes } from '@zooniverse/panoptes-js'
+import getServerSideAPIHost from '@helpers/getServerSideAPIHost'
+import logToSentry from '@helpers/logger/logToSentry.js'
+
+
+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'
+ }
+}
+
+const endpoint = '/classifications/users'
+
+/* user.created_at is needed for allTimeQuery, and not always available on the logged in user object */
+async function fetchUserCreatedAt(userID) {
+ const { headers, host } = getServerSideAPIHost(env)
+ const userQuery = {
+ env,
+ id: userID
+ }
+ try {
+ const response = await panoptes.get(`/users`, userQuery, { ...headers }, host)
+ if (response.ok) {
+ return response.body.users[0].created_at.substring(0, 10)
+ }
+ } catch (error) {
+ console.error('Error loading user with id:', userID)
+ logToSentry(error)
+ }
+}
+
+/* Same technique as getDefaultDateRange() in lib-user */
+function formatSevenDaysStatsQuery() {
+ const today = new Date()
+ const todayDateString = today.toISOString().substring(0, 10)
+
+ const defaultStartDate = new Date()
+ const sevenDaysAgo = defaultStartDate.getUTCDate() - 6
+ defaultStartDate.setUTCDate(sevenDaysAgo)
+ const endDateString = defaultStartDate.toISOString().substring(0, 10)
+
+ const query = {
+ end_date: todayDateString, // "Today" in UTC Timezone
+ period: 'day',
+ start_date: endDateString // 7 Days ago
+ }
+
+ const statsQuery = new URLSearchParams(query).toString()
+ return statsQuery
+}
+
+/* Similar to getDateInterval() and StatsTabs in lib-user */
+function formatAllTimeStatsQuery(userCreatedAt) {
+ const today = new Date()
+ const todayDateString = today.toISOString().substring(0, 10)
+
+ let queryPeriod
+ const differenceInDays = (new Date(todayDateString) - new Date(userCreatedAt)) / (1000 * 60 * 60 * 24)
+ if (differenceInDays <= 31) queryPeriod = 'day'
+ else if (differenceInDays <= 183) queryPeriod = 'week'
+ else if (differenceInDays <= 1460) queryPeriod = 'month'
+ else queryPeriod = 'year'
+
+ const query = {
+ end_date: todayDateString,
+ period: queryPeriod,
+ start_date: userCreatedAt
+ }
+
+ const statsQuery = new URLSearchParams(query).toString()
+ return statsQuery
+}
+
+async function fetchStats({ endpoint, projectID, userID, authorization }) {
+ const headers = { authorization }
+ const host = statsHost(env)
+
+ try {
+ const userCreatedAt = await fetchUserCreatedAt(userID)
+
+ const sevenDaysQuery = formatSevenDaysStatsQuery()
+ const allTimeQuery = formatAllTimeStatsQuery(userCreatedAt)
+
+ const sevenDaysResponse = await fetch(`${host}${endpoint}/${userID}/?${sevenDaysQuery}&project_id=${projectID}`, { headers })
+ const sevenDaysStats = await sevenDaysResponse.json()
+
+ const allTimeResponse = await fetch(`${host}${endpoint}/${userID}/?${allTimeQuery}&project_id=${projectID}`, { headers })
+ const allTimeStats = await allTimeResponse.json()
+
+ return {
+ sevenDaysStats,
+ allTimeStats
+ }
+ } catch (error) {
+ console.error('Error fetching stats', error)
+ logToSentry(error)
+ }
+}
+
+export default function useYourProjectStats({ projectID, userID }) {
+ const authorization = usePanoptesAuth(userID)
+
+ // only fetch stats when a userID is available. Don't fetch if no user logged in.
+ const key = authorization && userID ? { endpoint, projectID, userID, authorization } : null
+ return useSWR(key, fetchStats, SWROptions)
+}
diff --git a/packages/app-project/src/shared/components/Stat/Stat.js b/packages/app-project/src/shared/components/Stat/Stat.js
index 8a3e8ac3df..6bb46613c0 100644
--- a/packages/app-project/src/shared/components/Stat/Stat.js
+++ b/packages/app-project/src/shared/components/Stat/Stat.js
@@ -3,16 +3,26 @@ import { number, string } from 'prop-types'
import AnimatedNumber from '@zooniverse/react-components/AnimatedNumber'
-function Stat ({ className, label, value }) {
+/*
+ valueLoading is passed from components with useSWR to
+ avoid rendering the value in AnimatedNumber while stats
+ data is still loading
+*/
+
+function Stat({ className, label, value, valueLoading }) {
return (
-
-
-
+ {valueLoading ? (
+
+ ) : (
+
+
+
+ )}
{
export default function DashboardContainer({ authUser }) {
const key = { endpoint: '/users/[id]', authUser }
const { data: user, isLoading: userLoading } = useSWR(key, fetchProfileBanner, SWROptions)
+ console.log('lib-user', user)
return (
Date: Fri, 15 Nov 2024 13:26:31 -0600
Subject: [PATCH 02/16] create a stats container component and a RequireUser
component
---
.../YourProjectStats/YourProjectStats.js | 57 ++++++++++++-------
.../YourProjectStats.stories.js | 31 +++++++++-
.../YourProjectStatsContainer.js | 34 +++++++++++
.../components/RequireUser/RequireUser.js | 16 ++++++
.../RequireUser/RequireUser.stories.js | 8 +++
.../src/shared/components/Stat/Stat.js | 26 +++------
6 files changed, 129 insertions(+), 43 deletions(-)
create mode 100644 packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStatsContainer.js
create mode 100644 packages/app-project/src/shared/components/RequireUser/RequireUser.js
create mode 100644 packages/app-project/src/shared/components/RequireUser/RequireUser.stories.js
diff --git a/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStats.js b/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStats.js
index b04edbafa3..daaa8a2d32 100644
--- a/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStats.js
+++ b/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStats.js
@@ -1,31 +1,31 @@
import { Box } from 'grommet'
-import { useContext } from 'react'
-import { MobXProviderContext, observer } from 'mobx-react'
+import Loader from '@zooniverse/react-components/Loader'
+import { array, bool, number, shape, string } from 'prop-types'
import Stat from '@shared/components/Stat'
-import useYourProjectStats from './useYourProjectStats.js'
+import RequireUser from '@shared/components/RequireUser/RequireUser.js'
-function storeMapper(store) {
- const { project, user } = store
-
- return {
- projectID: project.id,
- userID: user.id
+const defaultStatsData = {
+ allTimeStats: {
+ period: [],
+ total_count: 0
+ },
+ sevenDaysStats: {
+ period: [],
+ total_count: 0
}
}
-function YourProjectStats() {
- const { store } = useContext(MobXProviderContext)
- const { projectID, userID } = storeMapper(store)
-
- const { data, loading, error } = useYourProjectStats({ projectID, userID })
- // console.log(data)
-
+function YourProjectStats({ data = defaultStatsData, loading, error, userID}) {
return (
<>
{userID ? (
<>
- {error ? (
+ {loading ?
+
+
+
+ : error ? (
There was an error loading your stats
) : (
@@ -35,13 +35,26 @@ function YourProjectStats() {
)}
>
) : (
-
-
-
-
+
)}
>
)
}
-export default observer(YourProjectStats)
+export default YourProjectStats
+
+YourProjectStats.propTypes = {
+ data: shape({
+ allTimeStats: shape({
+ period: array,
+ total_count: number
+ }),
+ sevenDaysStats: shape({
+ period: array,
+ total_count: number
+ })
+ }),
+ loading: bool,
+ error: bool,
+ userID: string
+}
diff --git a/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStats.stories.js b/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStats.stories.js
index 50dade6109..03d025011a 100644
--- a/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStats.stories.js
+++ b/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStats.stories.js
@@ -1,8 +1,33 @@
import YourProjectStats from './YourProjectStats'
+const mockData = {
+ allTimeStats: {
+ period: [],
+ total_count: 84
+ },
+ sevenDaysStats: {
+ period: [],
+ total_count: 674328
+ }
+}
+
export default {
- title: 'Classify / YourProjectStats',
- component: YourProjectStats,
+ title: 'Project App / Screens / Classify / YourProjectStats',
+ component: YourProjectStats
}
-export const Default = {}
+export const NoUser = {
+ render: () =>
+}
+
+export const WithUser = {
+ render: () =>
+}
+
+export const Loading = {
+ render: () =>
+}
+
+export const Error = {
+ render: () =>
+}
diff --git a/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStatsContainer.js b/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStatsContainer.js
new file mode 100644
index 0000000000..6896ed5399
--- /dev/null
+++ b/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStatsContainer.js
@@ -0,0 +1,34 @@
+import { useContext } from 'react'
+import { MobXProviderContext, observer } from 'mobx-react'
+
+import useYourProjectStats from './useYourProjectStats.js'
+import YourProjectStats from './YourProjectStats.js'
+
+function storeMapper(store) {
+ const { project, user } = store
+
+ return {
+ projectID: project.id,
+ userID: user.id
+ }
+}
+
+/**
+ * This is a relatively simple container for ProjectStats, but data fetching
+ * and store observing are purposely separated from the presentational component
+ * styling and logic. Fetching user data requires authorization, making it
+ * complicated to use a mock library like MSW for useYourProjectStats() hook.
+*/
+
+function YourProjectStatsContainer() {
+ const { store } = useContext(MobXProviderContext)
+ const { projectID, userID } = storeMapper(store)
+
+ const { data, loading, error } = useYourProjectStats({ projectID, userID })
+
+ return (
+
+ )
+}
+
+export default observer(YourProjectStatsContainer)
diff --git a/packages/app-project/src/shared/components/RequireUser/RequireUser.js b/packages/app-project/src/shared/components/RequireUser/RequireUser.js
new file mode 100644
index 0000000000..f02336e304
--- /dev/null
+++ b/packages/app-project/src/shared/components/RequireUser/RequireUser.js
@@ -0,0 +1,16 @@
+import { Box } from 'grommet'
+import { useTranslation } from 'next-i18next'
+
+export default function RequireUser() {
+ const { t } = useTranslation('components')
+
+ return (
+
+ {t('RequireUser.text')}
+
+ )
+}
diff --git a/packages/app-project/src/shared/components/RequireUser/RequireUser.stories.js b/packages/app-project/src/shared/components/RequireUser/RequireUser.stories.js
new file mode 100644
index 0000000000..25ba42fa13
--- /dev/null
+++ b/packages/app-project/src/shared/components/RequireUser/RequireUser.stories.js
@@ -0,0 +1,8 @@
+import RequireUser from './RequireUser'
+
+export default {
+ title: 'Project App / shared / RequireUser',
+ component: RequireUser
+}
+
+export const Default = {}
diff --git a/packages/app-project/src/shared/components/Stat/Stat.js b/packages/app-project/src/shared/components/Stat/Stat.js
index 6bb46613c0..336a07631a 100644
--- a/packages/app-project/src/shared/components/Stat/Stat.js
+++ b/packages/app-project/src/shared/components/Stat/Stat.js
@@ -3,26 +3,16 @@ import { number, string } from 'prop-types'
import AnimatedNumber from '@zooniverse/react-components/AnimatedNumber'
-/*
- valueLoading is passed from components with useSWR to
- avoid rendering the value in AnimatedNumber while stats
- data is still loading
-*/
-
-function Stat({ className, label, value, valueLoading }) {
+function Stat({ className, label, value }) {
return (
- {valueLoading ? (
-
- ) : (
-
-
-
- )}
+
+
+
Date: Fri, 15 Nov 2024 15:18:58 -0600
Subject: [PATCH 03/16] create ClassificationsChart component with VisX
---
.../src/screens/ClassifyPage/ClassifyPage.js | 4 +-
.../YourProjectStats/YourProjectStats.js | 87 ++++++++---
.../YourProjectStats.stories.js | 25 ++-
.../YourProjectStatsContainer.js | 16 +-
.../components/ClassificationsChart.js | 143 ++++++++++++++++++
.../ClassificationsChartContainer.js | 76 ++++++++++
.../helpers/dateRangeHelpers.js | 21 +++
.../YourProjectStats/useYourProjectStats.js | 33 ++--
8 files changed, 351 insertions(+), 54 deletions(-)
create mode 100644 packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/components/ClassificationsChart.js
create mode 100644 packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/components/ClassificationsChartContainer.js
create mode 100644 packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/helpers/dateRangeHelpers.js
diff --git a/packages/app-project/src/screens/ClassifyPage/ClassifyPage.js b/packages/app-project/src/screens/ClassifyPage/ClassifyPage.js
index c9443d0b41..311aa33740 100644
--- a/packages/app-project/src/screens/ClassifyPage/ClassifyPage.js
+++ b/packages/app-project/src/screens/ClassifyPage/ClassifyPage.js
@@ -10,7 +10,7 @@ import ProjectStatistics from '@shared/components/ProjectStatistics'
import FinishedForTheDay from './components/FinishedForTheDay'
import RecentSubjects from './components/RecentSubjects'
import YourStats from './components/YourStats'
-import YourProjectStats from './components/YourProjectStats/YourProjectStats.js'
+import YourProjectStatsContainer from './components/YourProjectStats/YourProjectStatsContainer.js'
import StandardLayout from '@shared/components/StandardLayout'
import WorkflowAssignmentModal from './components/WorkflowAssignmentModal'
import WorkflowMenuModal from './components/WorkflowMenuModal'
@@ -116,7 +116,7 @@ function ClassifyPage({
columns={screenSize === 'small' ? ['auto'] : ['1fr', '2fr']}
gap={screenSize === 'small' ? 'small' : 'medium'}
>
-
+
diff --git a/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStats.js b/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStats.js
index daaa8a2d32..c07606e357 100644
--- a/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStats.js
+++ b/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStats.js
@@ -1,43 +1,83 @@
import { Box } from 'grommet'
import Loader from '@zooniverse/react-components/Loader'
-import { array, bool, number, shape, string } from 'prop-types'
+import { arrayOf, bool, object, number, shape, string } from 'prop-types'
+import { useTranslation } from 'next-i18next'
+import ContentBox from '@shared/components/ContentBox'
import Stat from '@shared/components/Stat'
import RequireUser from '@shared/components/RequireUser/RequireUser.js'
+import ClassificationsChartContainer from './components/ClassificationsChartContainer.js'
const defaultStatsData = {
allTimeStats: {
- period: [],
+ data: [{
+ count: 0,
+ period: []
+ }],
total_count: 0
},
sevenDaysStats: {
- period: [],
+ data: [{
+ count: 0,
+ period: []
+ }],
total_count: 0
}
}
-function YourProjectStats({ data = defaultStatsData, loading, error, userID}) {
+function YourProjectStats({
+ data = defaultStatsData,
+ loading = false,
+ error = undefined,
+ projectID = '',
+ userID = '',
+ userLogin = ''
+}) {
+ const { t } = useTranslation('screens')
+
+ const linkProps = {
+ externalLink: true,
+ href: `https://www.zooniverse.org/users/${userLogin}/stats?project_id=${projectID}`
+ }
+
return (
- <>
+
{userID ? (
<>
- {loading ?
-
-
-
- : error ? (
- There was an error loading your stats
- ) : (
-
-
-
+ {loading ? (
+
+
+
+ ) : error ? (
+
+ There was an error loading your stats.
+
+ ) : data ? (
+ <>
+
+
+
- )}
+
+ >
+ ) : null}
>
) : (
)}
- >
+
)
}
@@ -46,15 +86,22 @@ export default YourProjectStats
YourProjectStats.propTypes = {
data: shape({
allTimeStats: shape({
- period: array,
+ data: arrayOf(shape({
+ count: number,
+ period: string
+ })),
total_count: number
}),
sevenDaysStats: shape({
- period: array,
+ data: arrayOf(shape({
+ count: number,
+ period: string
+ })),
total_count: number
})
}),
loading: bool,
- error: bool,
+ error: object,
+ projectID: string,
userID: string
}
diff --git a/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStats.stories.js b/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStats.stories.js
index 03d025011a..c558603f5b 100644
--- a/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStats.stories.js
+++ b/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStats.stories.js
@@ -3,11 +3,11 @@ import YourProjectStats from './YourProjectStats'
const mockData = {
allTimeStats: {
period: [],
- total_count: 84
+ total_count: 37564
},
sevenDaysStats: {
period: [],
- total_count: 674328
+ total_count: 84
}
}
@@ -21,13 +21,28 @@ export const NoUser = {
}
export const WithUser = {
- render: () =>
+ render: () => (
+
+ )
}
export const Loading = {
- render: () =>
+ render: () => (
+
+ )
}
export const Error = {
- render: () =>
+ render: () => (
+
+ )
}
diff --git a/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStatsContainer.js b/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStatsContainer.js
index 6896ed5399..a06489761e 100644
--- a/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStatsContainer.js
+++ b/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStatsContainer.js
@@ -9,7 +9,8 @@ function storeMapper(store) {
return {
projectID: project.id,
- userID: user.id
+ userID: user.id,
+ userLogin: user.login
}
}
@@ -18,16 +19,23 @@ function storeMapper(store) {
* and store observing are purposely separated from the presentational component
* styling and logic. Fetching user data requires authorization, making it
* complicated to use a mock library like MSW for useYourProjectStats() hook.
-*/
+ */
function YourProjectStatsContainer() {
const { store } = useContext(MobXProviderContext)
- const { projectID, userID } = storeMapper(store)
+ const { projectID, userID, userLogin } = storeMapper(store)
const { data, loading, error } = useYourProjectStats({ projectID, userID })
return (
-
+
)
}
diff --git a/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/components/ClassificationsChart.js b/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/components/ClassificationsChart.js
new file mode 100644
index 0000000000..7baaadae5e
--- /dev/null
+++ b/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/components/ClassificationsChart.js
@@ -0,0 +1,143 @@
+import { arrayOf, string, shape, number } from 'prop-types'
+import styled, { css, useTheme } from 'styled-components'
+import { AxisBottom, AxisLeft } from '@visx/axis'
+import { Group } from '@visx/group'
+import { Bar } from '@visx/shape'
+import { Text } from '@visx/text'
+import { scaleBand, scaleLinear } from '@visx/scale'
+
+const StyledBarGroup = styled(Group)`
+ text {
+ display: none;
+ ${props => css`
+ fill: ${props.theme.global.colors['light-1']};
+ font-size: ${props.theme.text.small.size};
+ `}
+ }
+
+ &:hover rect,
+ &:focus rect {
+ ${props =>
+ css`
+ fill: ${props.theme.global.colors.brand};
+ `}
+ }
+
+ &:hover text,
+ &:focus text {
+ display: inline-block;
+ }
+`
+
+function ClassificationsChart({ stats = [] }) {
+ const theme = useTheme()
+
+ const HEIGHT = 300
+ const PADDING = 25
+ const WIDTH = 500
+ const xScale = scaleBand({
+ range: [0, WIDTH],
+ round: true,
+ domain: stats.map(stat => stat.longLabel),
+ padding: 0.1
+ })
+
+ const yScale = scaleLinear({
+ range: [HEIGHT - PADDING, 0],
+ round: true,
+ domain: [0, Math.max(...stats.map(stat => stat.count), 10)],
+ nice: true
+ })
+
+ const axisColour = theme.dark
+ ? theme.global.colors.text.dark
+ : theme.global.colors.text.light
+
+ function tickLabelProps() {
+ return {
+ 'aria-hidden': 'true',
+ dx: '-0.25em',
+ dy: '0.2em',
+ fill: axisColour,
+ fontFamily: theme.global.font.family,
+ fontSize: '1rem',
+ textAnchor: 'middle'
+ }
+ }
+
+ function shortDayLabels(dayName) {
+ const stat = stats.find(stat => stat.longLabel === dayName)
+ return stat.label
+ }
+
+ return (
+
+
+
+ )
+}
+
+ClassificationsChart.propTypes = {
+ stats: arrayOf(
+ shape({
+ alt: string,
+ count: number,
+ label: string,
+ longLabel: string,
+ period: string
+ })
+ )
+}
+
+export default ClassificationsChart
diff --git a/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/components/ClassificationsChartContainer.js b/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/components/ClassificationsChartContainer.js
new file mode 100644
index 0000000000..2e80cab00e
--- /dev/null
+++ b/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/components/ClassificationsChartContainer.js
@@ -0,0 +1,76 @@
+import ClassificationsChart from './ClassificationsChart.js'
+import { arrayOf, number, shape, string } from 'prop-types'
+import { useRouter } from 'next/router.js'
+
+import { getTodayDateString, getSevenDaysAgoDateString } from '../helpers/dateRangeHelpers.js'
+
+const defaultStatsData = {
+ data: [{
+ count: 0,
+ period: []
+ }],
+ total_count: 0
+}
+
+export default function ClassificationsChartContainer({ stats = defaultStatsData }) {
+ const router = useRouter()
+
+ // Similar to getCompleteData() in lib-user's bar chart.
+ // The data.period array returned from ERAS includes only days you've classified on
+ // but we want to display all seven days in the ClassificationsChart
+ const completeData = []
+ let index = 0
+
+ // Use the same date strings that were used in the sevenDaysAgo ERAS query
+ const todayDateString = getTodayDateString()
+ const sevenDaysAgoString = getSevenDaysAgoDateString()
+
+ // Loop to current date starting seven days ago and see if there's matching data returned from ERAS
+ let currentDate = new Date(sevenDaysAgoString)
+ const endDate = new Date(todayDateString)
+
+ while (currentDate <= endDate) {
+ const matchingData = stats.data.find(stat => {
+ const statPeriod = new Date(stat.period)
+ const match = currentDate.getUTCDate() === statPeriod.getUTCDate()
+ return match
+ })
+
+ if (matchingData) {
+ completeData.push({
+ index,
+ ...matchingData
+ })
+ } else {
+ completeData.push({
+ index,
+ period: currentDate.toISOString(),
+ count: 0
+ })
+ }
+
+ currentDate.setUTCDate(currentDate.getUTCDate() + 1)
+ index += 1
+ }
+
+ // Attach 'day of the week' labels to each stat
+ const statsWithLabels = completeData.map(({ count, period }) => {
+ const date = new Date(period)
+ const longLabel = date.toLocaleDateString(router?.locale, { weekday: 'long' })
+ const alt = `${longLabel}: ${count}`
+ const label = date.toLocaleDateString(router?.locale, { weekday: 'short' })
+ return { alt, count, label, longLabel, period }
+ })
+
+ return
+}
+
+ClassificationsChartContainer.propTypes = {
+ stats: shape({
+ data: arrayOf(shape({
+ count: number,
+ period: string
+ })),
+ total_count: number
+ })
+}
diff --git a/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/helpers/dateRangeHelpers.js b/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/helpers/dateRangeHelpers.js
new file mode 100644
index 0000000000..541327d514
--- /dev/null
+++ b/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/helpers/dateRangeHelpers.js
@@ -0,0 +1,21 @@
+/* Today in UTC */
+export function getTodayDateString() {
+ const today = new Date()
+ return today.toISOString().substring(0, 10)
+}
+
+export function getSevenDaysAgoDateString() {
+ const sevenDaysAgoDate = new Date()
+ const sevenDaysAgo = sevenDaysAgoDate.getUTCDate() - 6
+ sevenDaysAgoDate.setUTCDate(sevenDaysAgo)
+ return sevenDaysAgoDate.toISOString().substring(0, 10)
+}
+
+export function getQueryPeriod(endDate, startDate) {
+ const differenceInDays = (new Date(endDate) - new Date(startDate)) / (1000 * 60 * 60 * 24)
+
+ if (differenceInDays <= 31) return 'day'
+ else if (differenceInDays <= 183) return 'week'
+ else if (differenceInDays <= 1460) return 'month'
+ else return 'year'
+}
diff --git a/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/useYourProjectStats.js b/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/useYourProjectStats.js
index 6dd79fcd7b..96469b87e9 100644
--- a/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/useYourProjectStats.js
+++ b/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/useYourProjectStats.js
@@ -1,9 +1,10 @@
import useSWR from 'swr'
-import { usePanoptesAuth } from '@hooks'
import { env, panoptes } from '@zooniverse/panoptes-js'
import getServerSideAPIHost from '@helpers/getServerSideAPIHost'
import logToSentry from '@helpers/logger/logToSentry.js'
+import { usePanoptesAuth } from '@hooks'
+import { getTodayDateString, getSevenDaysAgoDateString, getQueryPeriod } from './helpers/dateRangeHelpers.js'
const SWROptions = {
revalidateIfStale: true,
@@ -44,35 +45,22 @@ async function fetchUserCreatedAt(userID) {
/* Same technique as getDefaultDateRange() in lib-user */
function formatSevenDaysStatsQuery() {
- const today = new Date()
- const todayDateString = today.toISOString().substring(0, 10)
-
- const defaultStartDate = new Date()
- const sevenDaysAgo = defaultStartDate.getUTCDate() - 6
- defaultStartDate.setUTCDate(sevenDaysAgo)
- const endDateString = defaultStartDate.toISOString().substring(0, 10)
+ const todayDateString = getTodayDateString()
+ const sevenDaysAgoString = getSevenDaysAgoDateString()
const query = {
- end_date: todayDateString, // "Today" in UTC Timezone
+ end_date: todayDateString,
period: 'day',
- start_date: endDateString // 7 Days ago
+ start_date: sevenDaysAgoString
}
- const statsQuery = new URLSearchParams(query).toString()
- return statsQuery
+ return new URLSearchParams(query).toString()
}
/* Similar to getDateInterval() and StatsTabs in lib-user */
function formatAllTimeStatsQuery(userCreatedAt) {
- const today = new Date()
- const todayDateString = today.toISOString().substring(0, 10)
-
- let queryPeriod
- const differenceInDays = (new Date(todayDateString) - new Date(userCreatedAt)) / (1000 * 60 * 60 * 24)
- if (differenceInDays <= 31) queryPeriod = 'day'
- else if (differenceInDays <= 183) queryPeriod = 'week'
- else if (differenceInDays <= 1460) queryPeriod = 'month'
- else queryPeriod = 'year'
+ const todayDateString = getTodayDateString()
+ const queryPeriod = getQueryPeriod(todayDateString, userCreatedAt)
const query = {
end_date: todayDateString,
@@ -80,8 +68,7 @@ function formatAllTimeStatsQuery(userCreatedAt) {
start_date: userCreatedAt
}
- const statsQuery = new URLSearchParams(query).toString()
- return statsQuery
+ return new URLSearchParams(query).toString()
}
async function fetchStats({ endpoint, projectID, userID, authorization }) {
From d669f0660742aeb9dee0e22763ebf5286cf772ac Mon Sep 17 00:00:00 2001
From: Delilah <23665803+goplayoutside3@users.noreply.github.com>
Date: Fri, 15 Nov 2024 16:16:25 -0600
Subject: [PATCH 04/16] fix styling of ClassificationChart
---
.../YourProjectStats/YourProjectStats.js | 77 +++++++++++--------
.../YourProjectStats.stories.js | 30 +++++++-
.../components/ClassificationsChart.js | 61 +++++++--------
.../ClassificationsChartContainer.js | 22 ++----
.../helpers/dateRangeHelpers.js | 10 +--
.../YourProjectStats/useYourProjectStats.js | 4 +-
6 files changed, 114 insertions(+), 90 deletions(-)
diff --git a/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStats.js b/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStats.js
index c07606e357..f7e7f0bc07 100644
--- a/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStats.js
+++ b/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStats.js
@@ -1,7 +1,6 @@
import { Box } from 'grommet'
import Loader from '@zooniverse/react-components/Loader'
import { arrayOf, bool, object, number, shape, string } from 'prop-types'
-import { useTranslation } from 'next-i18next'
import ContentBox from '@shared/components/ContentBox'
import Stat from '@shared/components/Stat'
@@ -10,17 +9,21 @@ import ClassificationsChartContainer from './components/ClassificationsChartCont
const defaultStatsData = {
allTimeStats: {
- data: [{
- count: 0,
- period: []
- }],
+ data: [
+ {
+ count: 0,
+ period: ''
+ }
+ ],
total_count: 0
},
sevenDaysStats: {
- data: [{
- count: 0,
- period: []
- }],
+ data: [
+ {
+ count: 0,
+ period: ''
+ }
+ ],
total_count: 0
}
}
@@ -33,8 +36,6 @@ function YourProjectStats({
userID = '',
userLogin = ''
}) {
- const { t } = useTranslation('screens')
-
const linkProps = {
externalLink: true,
href: `https://www.zooniverse.org/users/${userLogin}/stats?project_id=${projectID}`
@@ -57,21 +58,27 @@ function YourProjectStats({
There was an error loading your stats.
) : data ? (
- <>
-
-
-
+
+
+
+
+
+
-
- >
) : null}
>
) : (
@@ -86,17 +93,21 @@ export default YourProjectStats
YourProjectStats.propTypes = {
data: shape({
allTimeStats: shape({
- data: arrayOf(shape({
- count: number,
- period: string
- })),
+ data: arrayOf(
+ shape({
+ count: number,
+ period: string
+ })
+ ),
total_count: number
}),
sevenDaysStats: shape({
- data: arrayOf(shape({
- count: number,
- period: string
- })),
+ data: arrayOf(
+ shape({
+ count: number,
+ period: string
+ })
+ ),
total_count: number
})
}),
diff --git a/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStats.stories.js b/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStats.stories.js
index c558603f5b..ef56684b96 100644
--- a/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStats.stories.js
+++ b/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/YourProjectStats.stories.js
@@ -1,13 +1,35 @@
import YourProjectStats from './YourProjectStats'
+import {
+ getTodayDateString,
+ getNumDaysAgoDateString
+} from './helpers/dateRangeHelpers'
+
+// Mock stats data of a user who classified on three days in the past seven days
+const sevenDaysAgoString = getNumDaysAgoDateString(6)
+const threeDaysAgoString = getNumDaysAgoDateString(2)
+const todayDateString = getTodayDateString()
+
const mockData = {
allTimeStats: {
- period: [],
- total_count: 37564
+ total_count: 9436
},
sevenDaysStats: {
- period: [],
- total_count: 84
+ data: [
+ {
+ count: 5,
+ period: sevenDaysAgoString
+ },
+ {
+ count: 23,
+ period: threeDaysAgoString
+ },
+ {
+ count: 12,
+ period: todayDateString
+ }
+ ],
+ total_count: 40
}
}
diff --git a/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/components/ClassificationsChart.js b/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/components/ClassificationsChart.js
index 7baaadae5e..72398b1a70 100644
--- a/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/components/ClassificationsChart.js
+++ b/packages/app-project/src/screens/ClassifyPage/components/YourProjectStats/components/ClassificationsChart.js
@@ -7,14 +7,6 @@ import { Text } from '@visx/text'
import { scaleBand, scaleLinear } from '@visx/scale'
const StyledBarGroup = styled(Group)`
- text {
- display: none;
- ${props => css`
- fill: ${props.theme.global.colors['light-1']};
- font-size: ${props.theme.text.small.size};
- `}
- }
-
&:hover rect,
&:focus rect {
${props =>
@@ -25,10 +17,19 @@ const StyledBarGroup = styled(Group)`
&:hover text,
&:focus text {
- display: inline-block;
+ display: block;
}
`
+const StyledBarLabel = styled(Text)`
+ display: none; // hide until bar is hovered or focused
+
+ ${props => css`
+ fill: ${props.theme.global.colors['neutral-1']};
+ font-size: 1rem;
+ `}
+`
+
function ClassificationsChart({ stats = [] }) {
const theme = useTheme()
@@ -50,19 +51,17 @@ function ClassificationsChart({ stats = [] }) {
})
const axisColour = theme.dark
- ? theme.global.colors.text.dark
- : theme.global.colors.text.light
+ ? theme.global.colors.white
+ : theme.global.colors.black
- function tickLabelProps() {
- return {
- 'aria-hidden': 'true',
- dx: '-0.25em',
- dy: '0.2em',
- fill: axisColour,
- fontFamily: theme.global.font.family,
- fontSize: '1rem',
- textAnchor: 'middle'
- }
+ const tickLabelProps = {
+ 'aria-hidden': 'true',
+ dx: '-0.25em',
+ dy: '0.2em',
+ fill: axisColour,
+ fontFamily: theme.global.font.family,
+ fontSize: '1rem',
+ textAnchor: 'middle'
}
function shortDayLabels(dayName) {
@@ -73,10 +72,8 @@ function ClassificationsChart({ stats = [] }) {
return (