diff --git a/packages/app-root/public/assets/LTSignature.png b/packages/app-root/public/assets/LTSignature.png
new file mode 100644
index 0000000000..3ebdbf5f25
Binary files /dev/null and b/packages/app-root/public/assets/LTSignature.png differ
diff --git a/packages/app-root/src/app/users/[login]/groups/MyGroupsContainer.js b/packages/app-root/src/app/users/[login]/groups/MyGroupsContainer.js
index bd540906da..dca115c9b1 100644
--- a/packages/app-root/src/app/users/[login]/groups/MyGroupsContainer.js
+++ b/packages/app-root/src/app/users/[login]/groups/MyGroupsContainer.js
@@ -4,7 +4,7 @@ import { MyGroups } from '@zooniverse/user'
import { useContext } from 'react'
import { PanoptesAuthContext } from '../../../../contexts'
-import AuthenticatedUsersPageContainer from '../components/AuthenticatedUsersPageContainer'
+import AuthenticatedUsersPageContainer from '../../../../components/AuthenticatedUsersPageContainer'
function MyGroupsContainer({
login
diff --git a/packages/app-root/src/app/users/[login]/stats/UserStatsContainer.js b/packages/app-root/src/app/users/[login]/stats/UserStatsContainer.js
index 2695de1247..fd56539310 100644
--- a/packages/app-root/src/app/users/[login]/stats/UserStatsContainer.js
+++ b/packages/app-root/src/app/users/[login]/stats/UserStatsContainer.js
@@ -3,13 +3,19 @@
import { UserStats } from '@zooniverse/user'
import { useContext } from 'react'
-import { PanoptesAuthContext } from '../../../../contexts'
-import AuthenticatedUsersPageContainer from '../components/AuthenticatedUsersPageContainer'
+import { PanoptesAuthContext, UserStatsContext } from '../../../../contexts'
+import AuthenticatedUsersPageContainer from '../../../../components/AuthenticatedUsersPageContainer'
function UserStatsContainer({
login
}) {
const { adminMode, isLoading, user } = useContext(PanoptesAuthContext)
+ const {
+ selectedDateRange,
+ selectedProject,
+ setSelectedDateRange,
+ setSelectedProject
+ } = useContext(UserStatsContext)
return (
)
diff --git a/packages/app-root/src/app/users/[login]/stats/certificate/CertificateContainer.js b/packages/app-root/src/app/users/[login]/stats/certificate/CertificateContainer.js
new file mode 100644
index 0000000000..663b326d72
--- /dev/null
+++ b/packages/app-root/src/app/users/[login]/stats/certificate/CertificateContainer.js
@@ -0,0 +1,35 @@
+'use client'
+
+import { Certificate } from '@zooniverse/user'
+import { useContext } from 'react'
+
+import { PanoptesAuthContext, UserStatsContext } from '../../../../../contexts'
+import AuthenticatedUsersPageContainer from '../../../../../components/AuthenticatedUsersPageContainer'
+
+function CertificateContainer({
+ login
+}) {
+ const { adminMode, isLoading, user } = useContext(PanoptesAuthContext)
+ const {
+ selectedDateRange,
+ selectedProject
+ } = useContext(UserStatsContext)
+
+ return (
+
+
+
+ )
+}
+
+export default CertificateContainer
diff --git a/packages/app-root/src/app/users/[login]/stats/certificate/page.js b/packages/app-root/src/app/users/[login]/stats/certificate/page.js
new file mode 100644
index 0000000000..ba6c38b277
--- /dev/null
+++ b/packages/app-root/src/app/users/[login]/stats/certificate/page.js
@@ -0,0 +1,14 @@
+import CertificateContainer from './CertificateContainer'
+
+export const metadata = {
+ title: 'Certificate',
+ description: 'My Zooniverse certificate page'
+}
+
+export default function Certificate({ params }) {
+ return (
+
+ )
+}
diff --git a/packages/app-root/src/app/users/[login]/stats/layout.js b/packages/app-root/src/app/users/[login]/stats/layout.js
new file mode 100644
index 0000000000..dbeee24759
--- /dev/null
+++ b/packages/app-root/src/app/users/[login]/stats/layout.js
@@ -0,0 +1,9 @@
+import UserStatsContextProvider from '../../../../components/UserStatsContextProvider'
+
+export default function UserStatsLayout({ children }) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/packages/app-root/src/app/users/[login]/components/AuthenticatedUsersPageContainer.js b/packages/app-root/src/components/AuthenticatedUsersPageContainer.js
similarity index 100%
rename from packages/app-root/src/app/users/[login]/components/AuthenticatedUsersPageContainer.js
rename to packages/app-root/src/components/AuthenticatedUsersPageContainer.js
diff --git a/packages/app-root/src/components/UserStatsContextProvider.js b/packages/app-root/src/components/UserStatsContextProvider.js
new file mode 100644
index 0000000000..d8129e1255
--- /dev/null
+++ b/packages/app-root/src/components/UserStatsContextProvider.js
@@ -0,0 +1,27 @@
+'use client'
+
+import { useContext } from 'react'
+
+import { PanoptesAuthContext, UserStatsContext } from '../contexts'
+import { useStatsDateRange, useStatsProject } from '../hooks'
+
+function UserStatsContextProvider({ children }) {
+ const { isLoading, user } = useContext(PanoptesAuthContext)
+ const { selectedDateRange, setSelectedDateRange } = useStatsDateRange({ isLoading, user })
+ const { selectedProject, setSelectedProject } = useStatsProject({ isLoading, user })
+
+ const statsContext = {
+ selectedDateRange,
+ selectedProject,
+ setSelectedDateRange,
+ setSelectedProject
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+export default UserStatsContextProvider
diff --git a/packages/app-root/src/contexts/UserStatsContext.js b/packages/app-root/src/contexts/UserStatsContext.js
new file mode 100644
index 0000000000..776ff67847
--- /dev/null
+++ b/packages/app-root/src/contexts/UserStatsContext.js
@@ -0,0 +1,5 @@
+import { createContext } from 'react'
+
+const UserStatsContext = createContext({})
+
+export default UserStatsContext
diff --git a/packages/app-root/src/contexts/index.js b/packages/app-root/src/contexts/index.js
index 88e3911ba3..bf9fc3e20d 100644
--- a/packages/app-root/src/contexts/index.js
+++ b/packages/app-root/src/contexts/index.js
@@ -1,2 +1,3 @@
export { default as PanoptesAuthContext } from './PanoptesAuthContext.js'
export { default as ThemeModeContext } from './ThemeModeContext.js'
+export { default as UserStatsContext } from './UserStatsContext.js'
diff --git a/packages/app-root/src/hooks/index.js b/packages/app-root/src/hooks/index.js
index e178f6d0c7..654a10f187 100644
--- a/packages/app-root/src/hooks/index.js
+++ b/packages/app-root/src/hooks/index.js
@@ -1,2 +1,4 @@
export { default as useAdminMode } from './useAdminMode.js'
export { default as usePreferredTheme } from './usePreferredTheme.js'
+export { default as useStatsDateRange } from './useStatsDateRange.js'
+export { default as useStatsProject } from './useStatsProject.js'
diff --git a/packages/app-root/src/hooks/useStatsDateRange.js b/packages/app-root/src/hooks/useStatsDateRange.js
new file mode 100644
index 0000000000..93ffea0c7d
--- /dev/null
+++ b/packages/app-root/src/hooks/useStatsDateRange.js
@@ -0,0 +1,26 @@
+import { useEffect, useState } from 'react'
+
+const isBrowser = typeof window !== 'undefined'
+const localStorage = isBrowser ? window.localStorage : null
+
+let initialDateRange = 'Last7Days'
+if (isBrowser) {
+ initialDateRange = localStorage?.getItem('selectedDateRange') || initialDateRange
+}
+
+export default function useStatsDateRange({ isLoading, user }) {
+ const [selectedDateRange, setSelectedDateRange] = useState(initialDateRange)
+
+ useEffect(function onDateRangeChange() {
+ localStorage?.setItem('selectedDateRange', selectedDateRange)
+ }, [selectedDateRange])
+
+ useEffect(function onUserChange() {
+ // when a user successfully logs out isLoading is false and user is undefined
+ if (!isLoading && !user?.login) {
+ setSelectedDateRange('Last7Days')
+ }
+ }, [isLoading, user?.login])
+
+ return { selectedDateRange, setSelectedDateRange }
+}
diff --git a/packages/app-root/src/hooks/useStatsProject.js b/packages/app-root/src/hooks/useStatsProject.js
new file mode 100644
index 0000000000..17bc261aa3
--- /dev/null
+++ b/packages/app-root/src/hooks/useStatsProject.js
@@ -0,0 +1,26 @@
+import { useEffect, useState } from 'react'
+
+const isBrowser = typeof window !== 'undefined'
+const localStorage = isBrowser ? window.localStorage : null
+
+let initialProject = 'AllProjects'
+if (isBrowser) {
+ initialProject = localStorage?.getItem('selectedProject') || initialProject
+}
+
+export default function useStatsProject({ isLoading, user }) {
+ const [selectedProject, setSelectedProject] = useState(initialProject)
+
+ useEffect(function onProjectChange() {
+ localStorage?.setItem('selectedProject', selectedProject)
+ }, [selectedProject])
+
+ useEffect(function onUserChange() {
+ // when a user successfully logs out isLoading is false and user is undefined
+ if (!isLoading && !user?.login) {
+ setSelectedProject('AllProjects')
+ }
+ }, [isLoading, user?.login])
+
+ return { selectedProject, setSelectedProject }
+}
diff --git a/packages/lib-react-components/CHANGELOG.md b/packages/lib-react-components/CHANGELOG.md
index b0bfe6ef6a..05ae88be96 100644
--- a/packages/lib-react-components/CHANGELOG.md
+++ b/packages/lib-react-components/CHANGELOG.md
@@ -1,9 +1,16 @@
# Change Log
+
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
+## [Unreleased]
+
+### Changed
+
+- ZooFooter and ZooHeader styling to include `display: none;` when printing (`@media print`)
+
## [1.13.0] 2024-05-17
### Added
diff --git a/packages/lib-react-components/src/ZooFooter/ZooFooter.js b/packages/lib-react-components/src/ZooFooter/ZooFooter.js
index 28acc1a494..f70c84e0be 100644
--- a/packages/lib-react-components/src/ZooFooter/ZooFooter.js
+++ b/packages/lib-react-components/src/ZooFooter/ZooFooter.js
@@ -1,6 +1,7 @@
import { Box, Grid } from 'grommet'
import { arrayOf, node, string } from 'prop-types'
import { useEffect } from 'react'
+import styled from 'styled-components'
import i18n, { useTranslation } from '../translations/i18n'
import { useHasMounted } from '../hooks'
@@ -11,6 +12,13 @@ import {
SocialAnchor
} from './components'
+const StyledFooter = styled(Box)`
+ // hide the footer when printing, added for the user stats certificate, but applies generally
+ @media print {
+ display: none;
+ }
+`
+
const defaultProps = {
aboutNavListURLs: [
'https://www.zooniverse.org/about',
@@ -112,8 +120,8 @@ export default function ZooFooter({
const talkNavListLabels = [t('ZooFooter.talkLabels.talk')]
return (
-
{hasMounted && adminContainer}
-
+
)
}
diff --git a/packages/lib-react-components/src/ZooHeader/ZooHeader.js b/packages/lib-react-components/src/ZooHeader/ZooHeader.js
index 29a796c963..9b7f9e7b68 100644
--- a/packages/lib-react-components/src/ZooHeader/ZooHeader.js
+++ b/packages/lib-react-components/src/ZooHeader/ZooHeader.js
@@ -15,6 +15,11 @@ import ZooniverseLogo from '../ZooniverseLogo'
export const StyledHeader = styled(Box)`
color: #b2b2b2;
font-size: 1em;
+
+ // hide the header when printing, added for the user stats certificate, but applies generally
+ @media print {
+ display: none;
+ }
`
export const StyledLogoAnchor = styled(Anchor)`
diff --git a/packages/lib-user/dev/components/App/App.js b/packages/lib-user/dev/components/App/App.js
index 374a164169..589ee61bcd 100644
--- a/packages/lib-user/dev/components/App/App.js
+++ b/packages/lib-user/dev/components/App/App.js
@@ -6,6 +6,7 @@ import { string } from 'prop-types'
import { useEffect, useState } from 'react'
import {
+ Certificate,
Contributors,
GroupStats,
MyGroups,
@@ -27,6 +28,8 @@ function App({
const [activeIndex, setActiveIndex] = useState(-1)
const [dark, setDarkTheme] = useState(false)
const [loading, setLoading] = useState(false)
+ const [selectedDateRange, setSelectedDateRange] = useState('Last7Days')
+ const [selectedProject, setSelectedProject] = useState('AllProjects')
const [user, setUser] = useState(null)
useEffect(() => {
@@ -76,7 +79,9 @@ function App({
/stats - user stats page
@@ -130,12 +135,27 @@ function App({
if (login === '[login]') {
content = In the url query param ?users=
, please replace [login]
with a user login.
} else if (subpaths[1] === 'stats') {
- content = (
-
- )
+ if (subpaths[2] === 'certificate') {
+ content = (
+
+ )
+ } else {
+ content = (
+
+ )
+ }
} else if (subpaths[1] === 'groups') {
content = (
-
+
lib-user - dev app
diff --git a/packages/lib-user/dev/index.html b/packages/lib-user/dev/index.html
index 958af80c36..631042e654 100644
--- a/packages/lib-user/dev/index.html
+++ b/packages/lib-user/dev/index.html
@@ -9,6 +9,12 @@
padding: 0;
box-sizing: border-box;
}
+
+ @media print {
+ .dev-app-header {
+ display: none;
+ }
+ }
diff --git a/packages/lib-user/src/components/Certificate/Certificate.js b/packages/lib-user/src/components/Certificate/Certificate.js
new file mode 100644
index 0000000000..a4e197e1a5
--- /dev/null
+++ b/packages/lib-user/src/components/Certificate/Certificate.js
@@ -0,0 +1,277 @@
+import { SpacedText, ZooniverseLogo } from '@zooniverse/react-components'
+import { Box } from 'grommet'
+import { number, string } from 'prop-types'
+import styled from 'styled-components'
+
+import { getDateInterval } from '@utils'
+
+import {
+ ContentBox,
+ HeaderLink,
+ Layout
+} from '@components/shared'
+
+import { formatDateRange } from './helpers/formatDateRange'
+
+const PrintableBox = styled(Box)`
+ font-size: 16px;
+
+ #certificate {
+ box-shadow: 1px 1px 4px 0px rgba(0, 0, 0, 0.25);
+ }
+
+ @media print {
+ > *:not(#certificate) {
+ visibility: hidden;
+ }
+
+ #certificate, #certificate * {
+ visibility: visible;
+ }
+
+ #certificate {
+ box-shadow: none;
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ }
+ }
+
+ @page {
+ margin: 0;
+ size: 'landscape';
+ }
+
+ .userName {
+ letter-spacing: 6px;
+ }
+
+ .userHours {
+ letter-spacing: 8px;
+ }
+`
+
+function handleClickPrint() {
+ window.print()
+}
+
+function Certificate({
+ creditedName = '',
+ displayName = '',
+ hours = 0,
+ login = '',
+ projectDisplayName = '',
+ projectsCount = 0,
+ selectedDateRange = 'AllTime'
+}) {
+ const { start_date, end_date } = getDateInterval(selectedDateRange)
+
+ const formattedDateRange = formatDateRange(start_date, end_date)
+
+ return (
+
+
+ }
+ >
+
+
+
+
+
+
+ This is to certify that
+
+
+ {creditedName || displayName}
+
+
+ has contributed
+
+
+ {hours}
+
+
+ hours
+
+
+
+ to
+
+ {projectsCount ? (
+ `${projectsCount} projects`
+ ) : (
+ projectDisplayName
+ )}
+
+
+
+ during
+
+ {formattedDateRange}
+
+
+
+
+ hosted by the online citizen science platform Zooniverse
+
+
+
+
+
+ Dr. Laura Trouille
+
+
+ Zooniverse PI
+
+
+ contact@zooniverse.org
+
+
+
+
+
+
+
+
+ )
+}
+
+Certificate.propTypes = {
+ creditedName: string,
+ displayName: string,
+ hours: number,
+ login: string,
+ projectDisplayName: string,
+ projectsCount: number,
+ selectedDateRange: string
+}
+
+export default Certificate
diff --git a/packages/lib-user/src/components/Certificate/Certificate.stories.js b/packages/lib-user/src/components/Certificate/Certificate.stories.js
new file mode 100644
index 0000000000..0d00c11ba6
--- /dev/null
+++ b/packages/lib-user/src/components/Certificate/Certificate.stories.js
@@ -0,0 +1,53 @@
+import Certificate from './Certificate'
+
+export default {
+ title: 'Components/Certificate',
+ component: Certificate
+}
+
+export const Default = {
+ args: {
+ creditedName: 'Example T. User',
+ displayName: 'ExampleUser',
+ hours: 45,
+ login: 'testUser',
+ projectsCount: 67,
+ selectedDateRange: 'AllTime',
+ selectedProject: 'AllProjects'
+ }
+}
+
+export const ProjectSpecific = {
+ args: {
+ creditedName: 'Example T. User',
+ displayName: 'Example User',
+ hours: 45,
+ login: 'testUser',
+ selectedDateRange: 'AllTime',
+ selectedProject: 'Galaxy Zoo'
+ }
+}
+
+export const Last7Days = {
+ args: {
+ creditedName: 'Example T. User',
+ displayName: 'Example User',
+ hours: 45,
+ login: 'testUser',
+ projectsCount: 67,
+ selectedDateRange: 'Last7Days',
+ selectedProject: 'AllProjects'
+ }
+}
+
+export const Last30Days = {
+ args: {
+ creditedName: 'Example T. User',
+ displayName: 'Example User',
+ hours: 45,
+ login: 'testUser',
+ projectsCount: 67,
+ selectedDateRange: 'Last30Days',
+ selectedProject: 'AllProjects'
+ }
+}
diff --git a/packages/lib-user/src/components/Certificate/CertificateContainer.js b/packages/lib-user/src/components/Certificate/CertificateContainer.js
new file mode 100644
index 0000000000..ba81c0fd07
--- /dev/null
+++ b/packages/lib-user/src/components/Certificate/CertificateContainer.js
@@ -0,0 +1,81 @@
+'use client'
+
+import { shape, string } from 'prop-types'
+
+import {
+ usePanoptesProjects,
+ useStats
+} from '@hooks'
+
+import {
+ convertStatsSecondsToHours,
+ getDateInterval
+} from '@utils'
+
+import Certificate from './Certificate'
+
+const STATS_ENDPOINT = '/classifications/users'
+
+function CertificateContainer({
+ authUser,
+ login,
+ selectedDateRange = 'AllTime',
+ selectedProject = 'AllProjects'
+}) {
+ // TODO: fetch user data if authUser is not login user (admin view)
+
+ // fetch stats
+ const statsQuery = getDateInterval(selectedDateRange)
+ statsQuery.time_spent = true
+ if (selectedProject !== 'AllProjects') {
+ statsQuery.project_id = parseInt(selectedProject)
+ } else {
+ statsQuery.project_contributions = true
+ }
+
+ const {
+ data: stats,
+ error: statsError,
+ isLoading: statsLoading
+ } = useStats({
+ endpoint: STATS_ENDPOINT,
+ sourceId: authUser?.id,
+ query: statsQuery
+ })
+
+ // fetch projects, if selectedProject is not 'AllProjects'
+ const projectIds = selectedProject !== 'AllProjects' ? [selectedProject] : null
+ const {
+ data: projects,
+ error: projectsError,
+ isLoading: projectsLoading
+ } = usePanoptesProjects(projectIds)
+
+ const hours = convertStatsSecondsToHours(stats?.time_spent)
+ const projectsCount = stats?.project_contributions?.length || 0
+ const projectDisplayName = projects?.[0]?.display_name
+
+ return (
+
+ )
+}
+
+CertificateContainer.propTypes = {
+ authUser: shape({
+ id: string,
+ login: string
+ }),
+ login: string,
+ selectedDateRange: string,
+ selectedProject: string,
+}
+
+export default CertificateContainer
diff --git a/packages/lib-user/src/components/Certificate/helpers/formatDateRange.js b/packages/lib-user/src/components/Certificate/helpers/formatDateRange.js
new file mode 100644
index 0000000000..ba1b0bbd69
--- /dev/null
+++ b/packages/lib-user/src/components/Certificate/helpers/formatDateRange.js
@@ -0,0 +1,17 @@
+export function formatDateRange(startDate, endDate) {
+ const start = new Date(startDate)
+ const end = new Date(endDate)
+
+ let formattedStartDate = `${start.toLocaleString('en-US', { month: 'short', timeZone: 'UTC' })} ${start.getUTCDate()}`
+ let formattedEndDate = `${end.toLocaleString('en-US', { month: 'short', timeZone: 'UTC' })} ${end.getUTCDate()}, ${end.getUTCFullYear()}`
+
+ if (start.getUTCFullYear() !== end.getUTCFullYear()) {
+ formattedStartDate += `, ${start.getUTCFullYear()} - `
+ } else if (start.getUTCMonth() !== end.getUTCMonth()) {
+ formattedStartDate += ` - `
+ } else {
+ formattedEndDate = ` - ${end.getUTCDate()}, ${end.getUTCFullYear()}`
+ }
+
+ return formattedStartDate + formattedEndDate
+}
diff --git a/packages/lib-user/src/components/Certificate/helpers/formatDateRange.spec.js b/packages/lib-user/src/components/Certificate/helpers/formatDateRange.spec.js
new file mode 100644
index 0000000000..822ded0d0a
--- /dev/null
+++ b/packages/lib-user/src/components/Certificate/helpers/formatDateRange.spec.js
@@ -0,0 +1,27 @@
+import { formatDateRange } from './formatDateRange'
+
+describe('components > Certificate > formatDateRange', function() {
+ it('should format date range with same month', function() {
+ const startDate = '2021-01-01'
+ const endDate = '2021-01-15'
+ const formattedDateRange = formatDateRange(startDate, endDate)
+
+ expect(formattedDateRange).to.equal('Jan 1 - 15, 2021')
+ })
+
+ it('should format date range with same year', function() {
+ const startDate = '2021-01-01'
+ const endDate = '2021-12-31'
+ const formattedDateRange = formatDateRange(startDate, endDate)
+
+ expect(formattedDateRange).to.equal('Jan 1 - Dec 31, 2021')
+ })
+
+ it('should format date range with different year', function() {
+ const startDate = '2021-01-01'
+ const endDate = '2022-01-31'
+ const formattedDateRange = formatDateRange(startDate, endDate)
+
+ expect(formattedDateRange).to.equal('Jan 1, 2021 - Jan 31, 2022')
+ })
+})
diff --git a/packages/lib-user/src/components/Certificate/index.js b/packages/lib-user/src/components/Certificate/index.js
new file mode 100644
index 0000000000..8d52ea00af
--- /dev/null
+++ b/packages/lib-user/src/components/Certificate/index.js
@@ -0,0 +1 @@
+export { default } from './CertificateContainer'
diff --git a/packages/lib-user/src/components/UserHome/components/SubjectCard/SubjectCard.spec.js b/packages/lib-user/src/components/UserHome/components/SubjectCard/SubjectCard.spec.js
index abd289a5fa..13ec196fa3 100644
--- a/packages/lib-user/src/components/UserHome/components/SubjectCard/SubjectCard.spec.js
+++ b/packages/lib-user/src/components/UserHome/components/SubjectCard/SubjectCard.spec.js
@@ -3,7 +3,7 @@ import { render, screen } from '@testing-library/react'
import Meta, { Default } from './SubjectCard.stories.js'
-describe('UserHome > compoents > SubjectCard', function () {
+describe('UserHome > components > SubjectCard', function () {
const DefaultStory = composeStory(Default, Meta)
it('should show the subject id', function () {
diff --git a/packages/lib-user/src/components/UserHome/components/SubjectCard/SubjectCard.stories.js b/packages/lib-user/src/components/UserHome/components/SubjectCard/SubjectCard.stories.js
index 99d4e4db1a..3e0e5e0b14 100644
--- a/packages/lib-user/src/components/UserHome/components/SubjectCard/SubjectCard.stories.js
+++ b/packages/lib-user/src/components/UserHome/components/SubjectCard/SubjectCard.stories.js
@@ -9,7 +9,7 @@ export default {
// .jpeg or .png
export const Default = {
args: {
- subjectID: 123,
+ subjectID: '123',
mediaSrc: 'https://panoptes-uploads-staging.zooniverse.org/subject_location/34d154ca-1d8a-4c8a-ae29-96f58e0700f4.jpeg',
projectSlug: 'brooke/i-fancy-cats'
},
@@ -26,7 +26,7 @@ export const Default = {
// .mp4
export const Video = {
args: {
- subjectID: 456,
+ subjectID: '456',
mediaSrc: 'https://panoptes-uploads.zooniverse.org/subject_location/7da396d3-04f6-44b9-aef5-db4cac5dc172.mp4',
projectSlug: 'sophiemu/solar-jet-hunter'
},
@@ -43,7 +43,7 @@ export const Video = {
// .txt
export const Text = {
args: {
- subjectID: 789,
+ subjectID: '789',
mediaSrc: 'https://panoptes-uploads.zooniverse.org/subject_location/f5506d1c-a0e9-4aba-a418-6a6c46a7731a.txt',
projectSlug: 'markb-panoptes/digileap-testing-staging'
},
@@ -60,7 +60,7 @@ export const Text = {
// .json
export const Data = {
args: {
- subjectID: 8364,
+ subjectID: '8364',
mediaSrc: 'https://panoptes-uploads.zooniverse.org/subject_location/bdc78b71-ce68-4e10-9ae7-0e643e215143.json',
projectSlug: 'cobalt-lensing/black-hole-hunters'
},
diff --git a/packages/lib-user/src/components/UserStats/UserStatsContainer.js b/packages/lib-user/src/components/UserStats/UserStatsContainer.js
index 50fdb6c52f..1233e05d08 100644
--- a/packages/lib-user/src/components/UserStats/UserStatsContainer.js
+++ b/packages/lib-user/src/components/UserStats/UserStatsContainer.js
@@ -1,7 +1,6 @@
'use client'
-import { shape, string } from 'prop-types'
-import { useState } from 'react'
+import { func, shape, string } from 'prop-types'
import {
usePanoptesProjects,
@@ -15,15 +14,17 @@ import {
import UserStats from './UserStats'
+const DEFAULT_HANDLER = () => true
const STATS_ENDPOINT = '/classifications/users'
function UserStatsContainer({
authUser,
login,
+ selectedDateRange = 'Last7Days',
+ selectedProject = 'AllProjects',
+ setSelectedDateRange = DEFAULT_HANDLER,
+ setSelectedProject = DEFAULT_HANDLER
}) {
- const [selectedProject, setSelectedProject] = useState('AllProjects')
- const [selectedDateRange, setSelectedDateRange] = useState('Last7Days')
-
// fetch user
const {
data: user,
@@ -73,14 +74,14 @@ function UserStatsContainer({
isLoading: projectsLoading
} = usePanoptesProjects(projectIDs)
- function handleProjectSelect (project) {
- setSelectedProject(project.value)
- }
-
function handleDateRangeSelect (dateRange) {
setSelectedDateRange(dateRange.value)
}
+ function handleProjectSelect (project) {
+ setSelectedProject(project.value)
+ }
+
return (
MyGroups > GroupForm', function() {
+describe('components > shared > GroupForm', function() {
describe('without a provided group (create mode)', function() {
const user = userEvent.setup()
diff --git a/packages/lib-user/src/components/shared/Layout/Layout.js b/packages/lib-user/src/components/shared/Layout/Layout.js
index 1d2c331d0b..acb7c0c125 100644
--- a/packages/lib-user/src/components/shared/Layout/Layout.js
+++ b/packages/lib-user/src/components/shared/Layout/Layout.js
@@ -29,6 +29,10 @@ const PageBody = styled(Box)`
border-radius: 8px 8px 0 0;
padding-bottom: 50px;
+ @media print {
+ margin-top: 0;
+ }
+
@media (width > 90rem) {
&::before {
content: '';
diff --git a/packages/lib-user/src/components/shared/Layout/components/PageHeader.js b/packages/lib-user/src/components/shared/Layout/components/PageHeader.js
index 69d6b04fc9..5388b16521 100644
--- a/packages/lib-user/src/components/shared/Layout/components/PageHeader.js
+++ b/packages/lib-user/src/components/shared/Layout/components/PageHeader.js
@@ -6,6 +6,12 @@ import styled from 'styled-components'
import HeaderDropdown from './HeaderDropdown'
+const StyledBox = styled(Box)`
+ @media print {
+ display: none;
+ }
+`
+
const HeaderItems = styled(Box)`
max-width: calc(90rem - 160px);
`
@@ -14,8 +20,8 @@ function PageHeader({ primaryHeaderItem = '', secondaryHeaderItems = [] }) {
const size = useContext(ResponsiveContext)
return (
-
-
+
)
}
diff --git a/packages/lib-user/src/components/shared/MainContent/MainContent.js b/packages/lib-user/src/components/shared/MainContent/MainContent.js
index 3d9d7a4809..826dc03cb4 100644
--- a/packages/lib-user/src/components/shared/MainContent/MainContent.js
+++ b/packages/lib-user/src/components/shared/MainContent/MainContent.js
@@ -1,6 +1,7 @@
-import { Box, Tab } from 'grommet'
+import { Box, Button, Tab } from 'grommet'
import { arrayOf, func, number, shape, string } from 'prop-types'
import { useState } from 'react'
+import styled from 'styled-components'
import {
convertStatsSecondsToHours,
@@ -26,6 +27,12 @@ const DEFAULT_SOURCE = {
display_name: '',
}
+const StyledButton = styled(Button)`
+ background-color: ${props => props.theme.global.colors['neutral-1']};
+ border-radius: 4px;
+ color: ${props => props.theme.global.colors['neutral-6']};
+`
+
function MainContent({
handleDateRangeSelect = DEFAULT_HANDLER,
handleProjectSelect = DEFAULT_HANDLER,
@@ -130,7 +137,12 @@ function MainContent({
gap='16px'
justify='end'
>
-
+
) : null}
diff --git a/packages/lib-user/src/components/shared/MainContent/MainContent.spec.js b/packages/lib-user/src/components/shared/MainContent/MainContent.spec.js
index 4bba0dd08f..06cc37d847 100644
--- a/packages/lib-user/src/components/shared/MainContent/MainContent.spec.js
+++ b/packages/lib-user/src/components/shared/MainContent/MainContent.spec.js
@@ -6,7 +6,7 @@ import { STATS } from '../../../../test/mocks/stats.mock.js'
import Meta, { Default } from './MainContent.stories.js'
-describe('components > UserStats > MainContent', function () {
+describe('components > shared > MainContent', function () {
const DefaultStory = composeStory(Default, Meta)
it('should show the user display name', function () {
diff --git a/packages/lib-user/src/components/shared/Tabs/theme.js b/packages/lib-user/src/components/shared/Tabs/theme.js
index 3374e42379..c7b6719d80 100644
--- a/packages/lib-user/src/components/shared/Tabs/theme.js
+++ b/packages/lib-user/src/components/shared/Tabs/theme.js
@@ -2,11 +2,9 @@ import { css } from 'styled-components'
const tabTheme = {
tab: {
- active: {
- color: {
- dark: 'brand',
- light: 'brand'
- },
+ color: {
+ dark: 'light-3',
+ light: 'dark-5'
},
border: {
active: {
diff --git a/packages/lib-user/src/components/shared/TopProjects/TopProjects.spec.js b/packages/lib-user/src/components/shared/TopProjects/TopProjects.spec.js
index 6be0653a4d..15c22c417c 100644
--- a/packages/lib-user/src/components/shared/TopProjects/TopProjects.spec.js
+++ b/packages/lib-user/src/components/shared/TopProjects/TopProjects.spec.js
@@ -5,7 +5,7 @@ import { PROJECTS } from '../../../../test/mocks/panoptes'
import Meta, { Default } from './TopProjects.stories.js'
-describe('components > UserStats > TopProjects', function () {
+describe('components > shared > TopProjects', function () {
const DefaultStory = composeStory(Default, Meta)
it('should show "Top Projects" content section title', function () {
diff --git a/packages/lib-user/src/index.js b/packages/lib-user/src/index.js
index 223cac5034..3e5ebe32be 100644
--- a/packages/lib-user/src/index.js
+++ b/packages/lib-user/src/index.js
@@ -1,7 +1,8 @@
// components
-export { default as Contributors } from './components/Contributors'
+export { default as Certificate } from './components/Certificate'
export { default as GroupStats } from './components/GroupStats'
export { default as MyGroups } from './components/MyGroups'
+export { default as Contributors } from './components/Contributors'
export { default as UserHome } from './components/UserHome'
export { default as UserStats } from './components/UserStats'
diff --git a/packages/lib-user/src/utils/getDateInterval.js b/packages/lib-user/src/utils/getDateInterval.js
index 8f24c2024d..8455e4e9a2 100644
--- a/packages/lib-user/src/utils/getDateInterval.js
+++ b/packages/lib-user/src/utils/getDateInterval.js
@@ -71,7 +71,9 @@ export function getDateInterval(dateRange) {
}
} else if (dateRange === 'AllTime') {
return {
- period: 'year'
+ end_date,
+ period: 'year',
+ start_date: '2015-07-01'
}
} else {
return {
diff --git a/packages/lib-user/src/utils/getDateInterval.spec.js b/packages/lib-user/src/utils/getDateInterval.spec.js
index 4a61d66b9e..bdb279dd26 100644
--- a/packages/lib-user/src/utils/getDateInterval.spec.js
+++ b/packages/lib-user/src/utils/getDateInterval.spec.js
@@ -61,7 +61,9 @@ describe('utils > getDateInterval', function () {
it('should return the expected date interval for the AllTime date range', function () {
const dateInterval = getDateInterval('AllTime')
expect(dateInterval).to.deep.equal({
+ end_date: '2023-04-15',
period: 'year',
+ start_date: '2015-07-01'
})
})
})