From c61a977e5e2284ec6dcca4861070615ab04b68f6 Mon Sep 17 00:00:00 2001 From: Evert-R Date: Tue, 28 May 2024 14:54:45 +0200 Subject: [PATCH] Add score and rank to header --- backend/experiment/serializers.py | 15 +++- backend/theme/serializers.py | 7 +- .../ExperimentCollection.tsx | 5 +- .../ExperimentCollectionDashboard.tsx | 15 ++-- frontend/src/components/Header/Header.tsx | 88 ++++++++++++++++++- frontend/src/types/Theme.ts | 3 +- 6 files changed, 119 insertions(+), 14 deletions(-) diff --git a/backend/experiment/serializers.py b/backend/experiment/serializers.py index 4bbe2d529..4ec2a57d4 100644 --- a/backend/experiment/serializers.py +++ b/backend/experiment/serializers.py @@ -50,12 +50,15 @@ def serialize_experiment_collection_group(group: ExperimentCollectionGroup, part next_experiment = get_upcoming_experiment( grouped_experiments, participant, group.dashboard) + total_score = get_total_score(grouped_experiments, participant) + if not next_experiment: return None return { 'dashboard': [serialize_experiment(experiment.experiment, participant) for experiment in grouped_experiments] if group.dashboard else [], - 'next_experiment': next_experiment + 'next_experiment': next_experiment, + 'total_score': total_score } @@ -93,3 +96,13 @@ def get_finished_session_count(experiment, participant): count = Session.objects.filter( experiment=experiment, participant=participant, finished_at__isnull=False).count() return count + + +def get_total_score(grouped_experiments, participant): + '''Calculate total score of all experiments on the dashboard''' + total_score = 0 + for grouped_experiment in grouped_experiments: + sessions = Session.objects.filter(experiment=grouped_experiment.experiment, participant=participant) + for session in sessions: + total_score += session.final_score + return total_score diff --git a/backend/theme/serializers.py b/backend/theme/serializers.py index 980b2bda3..00a3a9309 100644 --- a/backend/theme/serializers.py +++ b/backend/theme/serializers.py @@ -20,8 +20,11 @@ def serialize_header(header: HeaderConfig) -> dict: return { 'nextExperimentButtonText': _('Next experiment'), 'aboutButtonText': _('About us'), - 'showScore': header.show_score - } + 'showScore': header.show_score, + 'scoreClass': 'gold', + 'scoreLabel': _('Points'), + 'noScoreLabel': _('No points yet!') + } def serialize_theme(theme: ThemeConfig) -> dict: diff --git a/frontend/src/components/ExperimentCollection/ExperimentCollection.tsx b/frontend/src/components/ExperimentCollection/ExperimentCollection.tsx index 32f27472c..75412834b 100644 --- a/frontend/src/components/ExperimentCollection/ExperimentCollection.tsx +++ b/frontend/src/components/ExperimentCollection/ExperimentCollection.tsx @@ -34,7 +34,8 @@ const ExperimentCollection = ({ match }: ExperimentCollectionProps) => { const nextExperiment = experimentCollection?.next_experiment; const displayDashboard = experimentCollection?.dashboard.length; const showConsent = experimentCollection?.consent; - + const totalScore = experimentCollection?.total_score + const scoreClass = experimentCollection?.score_class const onNext = () => { setHasShownConsent(true); } @@ -72,7 +73,7 @@ const ExperimentCollection = ({ match }: ExperimentCollectionProps) => {
} /> - } /> + } />
) diff --git a/frontend/src/components/ExperimentCollection/ExperimentCollectionDashboard/ExperimentCollectionDashboard.tsx b/frontend/src/components/ExperimentCollection/ExperimentCollectionDashboard/ExperimentCollectionDashboard.tsx index e9ff1d349..71c8185a7 100644 --- a/frontend/src/components/ExperimentCollection/ExperimentCollectionDashboard/ExperimentCollectionDashboard.tsx +++ b/frontend/src/components/ExperimentCollection/ExperimentCollectionDashboard/ExperimentCollectionDashboard.tsx @@ -12,16 +12,19 @@ interface ExperimentCollectionDashboardProps { participantIdUrl: string | null; } -export const ExperimentCollectionDashboard: React.FC = ({ experimentCollection, participantIdUrl }) => { - +export const ExperimentCollectionDashboard: React.FC = ({ experimentCollection, participantIdUrl, totalScore }) => { + const dashboard = experimentCollection.dashboard; - const nextExperimentSlug = experimentCollection.nextExperiment?.slug; + const nextExperimentSlug = experimentCollection.nextExperiment?.slug; + const headerProps = experimentCollection.theme?.header? { - nextExperimentSlug, + nextExperimentSlug, collectionSlug: experimentCollection.slug, - ... experimentCollection.theme.header + ...experimentCollection.theme.header, + totalScore: totalScore + } : undefined; - + const getExperimentHref = (slug: string) => `/${slug}${participantIdUrl ? `?participant_id=${participantIdUrl}` : ""}`; return ( diff --git a/frontend/src/components/Header/Header.tsx b/frontend/src/components/Header/Header.tsx index f3018f02f..6be6346ac 100644 --- a/frontend/src/components/Header/Header.tsx +++ b/frontend/src/components/Header/Header.tsx @@ -1,15 +1,84 @@ -import React from "react"; +import React, { useEffect, useState, useRef } from "react"; import { Link } from "react-router-dom"; +import Rank from "../Rank/Rank"; +import Social from "../Social/Social" + interface HeaderProps { nextExperimentSlug: string | undefined; nextExperimentButtonText: string; collectionSlug: string; aboutButtonText: string; showScore: boolean; + totalScore: BigInteger; + scoreClass: string; + scoreLabel: string; + noScoreLabel: string; } -export const Header: React.FC = ({ nextExperimentSlug, nextExperimentButtonText, collectionSlug, aboutButtonText, showScore }) => { +export const Header: React.FC = ({ nextExperimentSlug, nextExperimentButtonText, collectionSlug, aboutButtonText, showScore, totalScore, scoreClass, scoreLabel, noScoreLabel }) => { + + const social = { + 'apps': ['facebook', 'twitter'], + 'message': `I scored ${totalScore} points`, + 'url': 'wwww.amsterdammusiclab.nl', + 'hashtags': ["amsterdammusiclab", "citizenscience"] + } + + const useAnimatedScore = (targetScore) => { + const [score, setScore] = useState(0); + + const scoreValue = useRef(0); + + useEffect(() => { + if (targetScore === 0) { + return; + } + + let id = -1; + + const nextStep = () => { + // Score step + const scoreStep = Math.max( + 1, + Math.min(10, Math.ceil(Math.abs(scoreValue.current - targetScore) / 10)) + ); + + // Scores are equal, stop + if (targetScore === scoreValue.current) { + return; + } + + // Add / subtract score + scoreValue.current += Math.sign(targetScore - scoreValue.current) * scoreStep; + setScore(scoreValue.current); + + id = setTimeout(nextStep, 50); + }; + id = setTimeout(nextStep, 50); + + return () => { + window.clearTimeout(id); + }; + }, [targetScore]); + + return score; + }; + + const Score = ({ score, label, scoreClass }) => { + const currentScore = useAnimatedScore(score); + + return ( +
+ +

+ {currentScore ? currentScore + " " : ""} + {label} +

+
+ ); + }; + return (
@@ -18,6 +87,21 @@ export const Header: React.FC = ({ nextExperimentSlug, nextExperime {aboutButtonText && {aboutButtonText}}
+ {showScore, totalScore !== 0 && ( +
+ + +
+ )} + {showScore, totalScore === 0 && ( +

{noScoreLabel}

+ )}
); } diff --git a/frontend/src/types/Theme.ts b/frontend/src/types/Theme.ts index b9aeae5df..9b674bd63 100644 --- a/frontend/src/types/Theme.ts +++ b/frontend/src/types/Theme.ts @@ -2,6 +2,7 @@ export interface Header { nextExperimentButtonText: string; aboutButtonText: string; showScore: boolean; + totalScore: BigInteger; }; export default interface Theme { @@ -13,4 +14,4 @@ export default interface Theme { name: string; footer: null; header: Header | null; -} \ No newline at end of file +}