+ );
+}
+
+
+
+export default Header;
diff --git a/frontend/src/components/Final/Final.test.jsx b/frontend/src/components/Final/Final.test.tsx
similarity index 89%
rename from frontend/src/components/Final/Final.test.jsx
rename to frontend/src/components/Final/Final.test.tsx
index 33c2876d8..3eff6a18c 100644
--- a/frontend/src/components/Final/Final.test.jsx
+++ b/frontend/src/components/Final/Final.test.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { vi } from 'vitest';
+import { vi, expect, describe, it } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { BrowserRouter, Router } from 'react-router-dom';
import { createMemoryHistory } from 'history'
@@ -52,8 +52,8 @@ describe('Final Component', () => {
);
- expect(screen.queryByText(/Final Text/i)).to.exist;
- expect(screen.queryByTestId('score')).to.exist;
+ expect(document.body.contains(screen.queryByText('Final Text'))).toBe(true);
+ expect(document.body.contains(screen.queryByTestId('score'))).toBe(true);
});
it('calls onNext prop when button is clicked', async () => {
@@ -85,8 +85,8 @@ describe('Final Component', () => {
);
- expect(screen.queryByText('Rank')).to.not.exist;
- expect(screen.queryByText('Social')).to.not.exist;
+ expect(document.body.contains(screen.queryByTestId('rank'))).toBe(false);
+ expect(document.body.contains(screen.queryByTestId('social'))).toBe(false);
});
it('navigates to profile page when profile link is clicked', async () => {
@@ -109,7 +109,7 @@ describe('Final Component', () => {
const profileLink = screen.getByTestId('profile-link');
- expect(profileLink).to.exist;
+ expect(document.body.contains(profileLink)).toBe(true);
expect(history.location.pathname).toBe('/');
@@ -145,7 +145,7 @@ describe('Final Component', () => {
);
const el = screen.getByTestId('button-link');
- expect(el).to.exist;
+ expect(document.body.contains(el)).toBe(true);
expect(el.getAttribute('href')).toBe('/redirect/aml');
});
@@ -159,7 +159,7 @@ describe('Final Component', () => {
);
const el = screen.getByTestId('button-link');
- expect(el).to.exist;
+ expect(document.body.contains(el)).toBe(true);
expect(el.getAttribute('href')).toBe('https://example.com');
});
diff --git a/frontend/src/components/Final/Final.jsx b/frontend/src/components/Final/Final.tsx
similarity index 72%
rename from frontend/src/components/Final/Final.jsx
rename to frontend/src/components/Final/Final.tsx
index b4a811333..1f16f9894 100644
--- a/frontend/src/components/Final/Final.jsx
+++ b/frontend/src/components/Final/Final.tsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect, useRef } from "react";
+import React, { useEffect } from "react";
import { withRouter } from "react-router-dom";
import Rank from "../Rank/Rank";
@@ -18,39 +18,8 @@ import FinalButton from "./FinalButton";
const Final = ({ block, participant, score, final_text, action_texts, button,
onNext, history, show_participant_link, participant_id_only,
show_profile_link, social, feedback_info, points, rank, logo }) => {
- const [showScore, setShowScore] = useState(0);
const session = useBoundStore((state) => state.session);
- // Use a ref to prevent doing multiple increments
- // when the render is skipped
- const scoreValue = useRef(0);
-
- useEffect(() => {
- if (score === 0) {
- return;
- }
-
- const id = setTimeout(() => {
- // Score step
- const scoreStep = Math.max(
- 1,
- Math.min(10, Math.ceil(Math.abs(scoreValue.current - score) / 10))
- );
-
- // Score are equal, stop
- if (score === scoreValue.current) {
- return;
- }
- // Add / subtract score
- scoreValue.current += Math.sign(score - scoreValue.current) * scoreStep;
- setShowScore(scoreValue.current);
- }, 50);
-
- return () => {
- clearTimeout(id);
- };
- }, [score, showScore]);
-
useEffect(() => {
finalizeSession({ session, participant });
}, [session, participant]);
@@ -59,8 +28,7 @@ const Final = ({ block, participant, score, final_text, action_texts, button,
{rank && (
-
-
{showScore} {points}
+
)}
diff --git a/frontend/src/components/Header/Header.tsx b/frontend/src/components/Header/Header.tsx
deleted file mode 100644
index d2a980253..000000000
--- a/frontend/src/components/Header/Header.tsx
+++ /dev/null
@@ -1,132 +0,0 @@
-import React, { useEffect, useState } from "react";
-import { Link } from "react-router-dom";
-
-import Rank from "../Rank/Rank";
-import Social from "../Social/Social"
-import HTML from '@/components/HTML/HTML';
-
-interface HeaderProps {
- experimentCollectionTitle: string;
- experimentCollectionDescription: string;
- nextBlockSlug: string | undefined;
- nextExperimentButtonText: string;
- collectionSlug: string;
- aboutButtonText: string;
- totalScore: number;
-}
-
-interface Score {
- scoreClass: string;
- scoreLabel: string;
- noScoreLabel: string;
-}
-
-export const Header: React.FC
= ({
- experimentCollectionTitle,
- experimentCollectionDescription,
- nextBlockSlug,
- nextExperimentButtonText,
- collectionSlug,
- aboutButtonText,
- totalScore,
- score,
-}) => {
-
- // TODO: Fix this permanently and localize in and fetch content from the backend
- // See also: https://github.com/Amsterdam-Music-Lab/MUSCLE/issues/1151
- // Get current URL minus the query string
- const currentUrl = window.location.href.split('?')[0];
- const message = totalScore > 0 ? `Ha! Ik ben muzikaler dan ik dacht - heb maar liefst ${totalScore} punten! Speel mee met #ToontjeHoger` : "Ha! Speel mee met #ToontjeHoger en laat je verrassen: je bent muzikaler dat je denkt!";
- const hashtags = [experimentCollectionTitle ? experimentCollectionTitle.replace(/ /g, '') : 'amsterdammusiclab'];
-
- const social = {
- apps: ['facebook', 'twitter'],
- message,
- url: currentUrl,
- hashtags,
- }
-
- const useAnimatedScore = (targetScore: number) => {
- const [score, setScore] = useState(0);
-
- useEffect(() => {
- if (targetScore === 0) {
- setScore(0);
- return;
- }
-
- let animationFrameId: number;
-
- const nextStep = () => {
- setScore((prevScore) => {
- const difference = targetScore - prevScore;
- const scoreStep = Math.max(1, Math.min(10, Math.ceil(Math.abs(difference) / 10)));
-
- if (difference === 0) {
- cancelAnimationFrame(animationFrameId);
- return prevScore;
- }
-
- const newScore = prevScore + Math.sign(difference) * scoreStep;
- animationFrameId = requestAnimationFrame(nextStep);
- return newScore;
- });
- };
-
- // Start the animation
- animationFrameId = requestAnimationFrame(nextStep);
-
- // Cleanup function to cancel the animation frame
- return () => {
- cancelAnimationFrame(animationFrameId);
- };
- }, [targetScore]);
-
- return score;
- };
-
- const Score = ({ score, label, scoreClass }) => {
- const currentScore = useAnimatedScore(score);
-
- return (
-
-
-
- {currentScore ? currentScore + " " : ""}
- {label}
-
-
- );
- };
-
- return (
-
- {score && totalScore !== 0 && (
-
-
-
-
- )}
- {score && totalScore === 0 && (
- {score.noScoreLabel}
- )}
-
- );
-}
-
-
-
-export default Header;
diff --git a/frontend/src/components/Profile/Profile.jsx b/frontend/src/components/Profile/Profile.jsx
index 9653b52d1..c8edaa312 100644
--- a/frontend/src/components/Profile/Profile.jsx
+++ b/frontend/src/components/Profile/Profile.jsx
@@ -2,7 +2,7 @@ import React from "react";
import { Link } from "react-router-dom";
import DefaultPage from "../Page/DefaultPage";
import Loading from "../Loading/Loading";
-import Rank from "../Rank/Rank";
+import Cup from "../Cup/Cup";
import { useParticipantScores } from "../../API";
import { URLS } from "@/config";
import ParticipantLink from "../ParticipantLink/ParticipantLink";
@@ -28,58 +28,7 @@ const Profile = () => {
);
break;
default: {
- // Highest score
- data.scores.sort((a, b) =>
- a.finished_at === b.finished_at
- ? 0
- : a.finished_at > b.finished_at
- ? -1
- : 1
- );
- view = (
- <>
-
{data.messages.title}
-
- {data.messages.summary}
-
-
-
-
- {data.scores.map((score, index) => (
-
-
-
-
-
-
-
- {score.experiment_name}
-
-
-
{score.score} {data.points}
-
{score.date}
-
-
- ))}
-
-
-
-
-
- {data.messages.continue}
-
-
-
- >
- );
+ view = ProfileView(data);
}
}
@@ -88,4 +37,61 @@ const Profile = () => {
return
{view};
};
+export const ProfileView = (data) => {
+
+ // Highest score
+ data.scores.sort((a, b) =>
+ a.finished_at === b.finished_at
+ ? 0
+ : a.finished_at > b.finished_at
+ ? -1
+ : 1
+ );
+
+ return (
+ <>
+
{data.messages.title}
+
+ {data.messages.summary}
+
+
+
+
+ {data.scores.map((score, index) => (
+
+
+
+
+
+
+
+ {score.experiment_name}
+
+
+
{score.score} {data.points}
+
{score.date}
+
+
+ ))}
+
+
+
+
+
+ {data.messages.continue}
+
+
+
+ >
+ )
+}
+
export default Profile;
diff --git a/frontend/src/components/Rank/Rank.jsx b/frontend/src/components/Rank/Rank.jsx
deleted file mode 100644
index f51edfb0e..000000000
--- a/frontend/src/components/Rank/Rank.jsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import React from "react";
-import classNames from "classnames";
-
-// Rank shows a decorated representation of a rank
-const Rank = ({ rank }) => {
- return (
-
- );
-};
-
-export default Rank;
diff --git a/frontend/src/components/Rank/Rank.test.tsx b/frontend/src/components/Rank/Rank.test.tsx
new file mode 100644
index 000000000..5be4abe3c
--- /dev/null
+++ b/frontend/src/components/Rank/Rank.test.tsx
@@ -0,0 +1,35 @@
+import { render, waitFor } from '@testing-library/react';
+import Rank from './Rank';
+import { describe, expect, it } from 'vitest';
+
+describe('Rank Component', () => {
+
+ it('renders the correct components', () => {
+ const cup = {
+ className: 'gold',
+ text: 'Gold Rank',
+ };
+
+ const score = {
+ score: 100,
+ label: 'Points',
+ }
+
+ const { getByTestId, getByText } = render(
);
+
+ const cupElement = getByTestId('cup');
+
+ // Check if the main div has the correct classes
+ expect(cupElement.classList.contains('aha__cup')).toBe(true);
+ expect(cupElement.classList.contains('gold')).toBe(true);
+ expect(cupElement.classList.contains('offsetCup')).toBe(true);
+
+ // Check if the h4 element contains the correct text
+ expect(document.body.contains(getByText('Gold Rank'))).toBe(true);
+
+ // Wait for the score to be rendered
+ waitFor(() => {
+ expect(document.body.contains(getByText('100 Points'))).toBe(true);
+ });
+ });
+});
diff --git a/frontend/src/components/Rank/Rank.tsx b/frontend/src/components/Rank/Rank.tsx
new file mode 100644
index 000000000..0e0f51c58
--- /dev/null
+++ b/frontend/src/components/Rank/Rank.tsx
@@ -0,0 +1,17 @@
+import ScoreCounter, { ScoreCounterProps } from "../ScoreCounter/ScoreCounter";
+import Cup, { CupProps } from "../Cup/Cup";
+
+interface RankProps {
+ cup: CupProps
+ score: ScoreCounterProps;
+}
+
+// Rank shows a decorated representation of a rank
+const Rank = ({ cup, score }: RankProps) => (
+
+
+
+
+);
+
+export default Rank;
diff --git a/frontend/src/components/ScoreCounter/ScoreCounter.tsx b/frontend/src/components/ScoreCounter/ScoreCounter.tsx
new file mode 100644
index 000000000..650b59e59
--- /dev/null
+++ b/frontend/src/components/ScoreCounter/ScoreCounter.tsx
@@ -0,0 +1,19 @@
+import useAnimatedScore from "@/hooks/useAnimatedScore";
+
+export interface ScoreCounterProps {
+ score: number;
+ label: string;
+}
+
+const ScoreCounter = ({ score, label }: ScoreCounterProps) => {
+ const currentScore = useAnimatedScore(score);
+
+ return (
+
+ {currentScore || currentScore === 0 ? currentScore + " " : ""}
+ {label}
+
+ );
+};
+
+export default ScoreCounter;
diff --git a/frontend/src/components/ToontjeHoger/ToontjeHoger.jsx b/frontend/src/components/ToontjeHoger/ToontjeHoger.jsx
index 38986c881..673fd2ab6 100644
--- a/frontend/src/components/ToontjeHoger/ToontjeHoger.jsx
+++ b/frontend/src/components/ToontjeHoger/ToontjeHoger.jsx
@@ -1,7 +1,7 @@
import React, { useEffect, useState, useRef } from "react";
import { LOGO_TITLE } from "@/config";
import { Switch, Route, Link } from "react-router-dom";
-import Rank from "../Rank/Rank";
+import Cup from "../Cup/Cup";
const LOGO_URL = "/images/experiments/toontjehoger/logo.svg";
@@ -143,7 +143,7 @@ const Score = ({ score, label, scoreClass }) => {
return (
-
+
{currentScore ? currentScore + " " : ""}
{label}
diff --git a/frontend/src/components/components.scss b/frontend/src/components/components.scss
index cda413e1e..01fe63c1e 100644
--- a/frontend/src/components/components.scss
+++ b/frontend/src/components/components.scss
@@ -8,7 +8,7 @@
@import "./Circle/Circle";
@import "./CountDown/CountDown";
@import "./Histogram/Histogram";
-@import "./Rank/Rank";
+@import "./Cup/Cup";
@import "./Logo/Logo";
// Profile
@@ -46,4 +46,4 @@
@import "./UserFeedback/UserFeedback";
// ExperimentCollection
-@import "./Footer/Footer";
\ No newline at end of file
+@import "./Footer/Footer";
diff --git a/frontend/src/hooks/useAnimatedScore.ts b/frontend/src/hooks/useAnimatedScore.ts
new file mode 100644
index 000000000..e2c92f734
--- /dev/null
+++ b/frontend/src/hooks/useAnimatedScore.ts
@@ -0,0 +1,42 @@
+import { useEffect, useState } from "react";
+
+const useAnimatedScore = (targetScore: number) => {
+ const [score, setScore] = useState(0);
+
+ useEffect(() => {
+ if (targetScore === 0) {
+ setScore(0);
+ return;
+ }
+
+ let animationFrameId: number;
+
+ const nextStep = () => {
+ setScore((prevScore) => {
+ const difference = targetScore - prevScore;
+ const scoreStep = Math.max(1, Math.min(10, Math.ceil(Math.abs(difference) / 10)));
+
+ if (difference === 0) {
+ cancelAnimationFrame(animationFrameId);
+ return prevScore;
+ }
+
+ const newScore = prevScore + Math.sign(difference) * scoreStep;
+ animationFrameId = requestAnimationFrame(nextStep);
+ return newScore;
+ });
+ };
+
+ // Start the animation
+ animationFrameId = requestAnimationFrame(nextStep);
+
+ // Cleanup function to cancel the animation frame
+ return () => {
+ cancelAnimationFrame(animationFrameId);
+ };
+ }, [targetScore]);
+
+ return score;
+};
+
+export default useAnimatedScore;
\ No newline at end of file
diff --git a/frontend/src/stories/Consent.stories.jsx b/frontend/src/stories/Consent.stories.jsx
index e912ef3cb..d34f437c0 100644
--- a/frontend/src/stories/Consent.stories.jsx
+++ b/frontend/src/stories/Consent.stories.jsx
@@ -8,7 +8,7 @@ const defaultArgs = {
},
confirm: "Confirm",
deny: "Deny",
- experiment: {
+ block: {
slug: "experiment-slug",
},
};
@@ -33,7 +33,7 @@ export const Default = {
},
confirm: "Confirm",
deny: "Deny",
- experiment: {
+ block: {
slug: "experiment-slug",
},
},
diff --git a/frontend/src/stories/Final.stories.jsx b/frontend/src/stories/Final.stories.jsx
index ab859ae19..7645b1cc3 100644
--- a/frontend/src/stories/Final.stories.jsx
+++ b/frontend/src/stories/Final.stories.jsx
@@ -48,7 +48,7 @@ function getFinalData(overrides = {}) {
contact_body:
'
Please contact us at ',
},
- experiment: {
+ block: {
slug: "test",
},
participant: "test",
@@ -103,4 +103,4 @@ export const NoButtonLink = {
},
}),
decorators: [getDecorator],
-};
\ No newline at end of file
+};
diff --git a/frontend/src/stories/Header.stories.tsx b/frontend/src/stories/Header.stories.tsx
new file mode 100644
index 000000000..d9a7b2e7d
--- /dev/null
+++ b/frontend/src/stories/Header.stories.tsx
@@ -0,0 +1,73 @@
+import { BrowserRouter as Router } from "react-router-dom";
+
+import Header from "../components/ExperimentCollection/Header/Header";
+
+export default {
+ title: "Header",
+ component: Header,
+ parameters: {
+ layout: "fullscreen",
+ },
+};
+
+function getHeaderData(overrides = {}) {
+ return {
+ description: "Experiment ABC
This is the experiment description
",
+ nextExperimentSlug: '/th1-mozart',
+ nextBlockButtonText: 'Volgende experiment',
+ collectionSlug: '/collection/thkids',
+ aboutButtonText: 'Over ons',
+ totalScore: 420,
+ scoreDisplayConfig: {
+ scoreClass: 'gold',
+ scoreLabel: 'points',
+ },
+ ...overrides,
+ };
+}
+
+const getDecorator = (Story) => (
+
+
+
+
+
+);
+
+export const Default = {
+ args: getHeaderData(),
+ decorators: [getDecorator],
+};
+
+export const ZeroScore = {
+ args: getHeaderData({ score: 0, score_message: "No points!" }),
+ decorators: [getDecorator],
+};
+
+export const NegativeScore = {
+ args: getHeaderData({ score: -100, score_message: "Incorrect!" }),
+ decorators: [getDecorator],
+};
+
+export const ScoreWithoutLabel = {
+ args: getHeaderData({ score_message: "" }),
+ decorators: [getDecorator],
+};
+
+export const CustomLabel = {
+ args: getHeaderData({ score_message: "points earned" }),
+ decorators: [getDecorator],
+};
+
+export const CustomScoreClass = {
+ args: getHeaderData({ scoreClass: "silver" }),
+ decorators: [getDecorator],
+};
+
+export const SelectableScoreClass = {
+ args: getHeaderData(),
+ decorators: [getDecorator],
+};
diff --git a/frontend/src/stories/ProfileView.stories.jsx b/frontend/src/stories/ProfileView.stories.jsx
new file mode 100644
index 000000000..496bbb190
--- /dev/null
+++ b/frontend/src/stories/ProfileView.stories.jsx
@@ -0,0 +1,61 @@
+import { BrowserRouter as Router } from "react-router-dom";
+
+import { ProfileView } from "../components/Profile/Profile";
+
+export default {
+ title: "ProfileView",
+ component: ProfileView,
+ parameters: {
+ layout: "fullscreen",
+ },
+};
+
+function getProfileData(overrides = {}) {
+ return {
+ messages: {
+ title: "Profile",
+ summary: "Knorrum bipsum sulfur bit dalmatian",
+ },
+ scores: [
+ {
+ finished_at: "2021-09-20T12:00:00Z",
+ rank: {
+ class: "silver",
+ text: "2nd",
+ },
+ score: 100,
+ points: "points",
+ date: "2021-09-20",
+ experiment_slug: "experiment-slug",
+ },
+ {
+ finished_at: "2021-09-21T12:00:00Z",
+ rank: {
+ class: "bronze",
+ text: "3rd",
+ },
+ score: 50,
+ points: "points",
+ date: "2021-09-21",
+ experiment_slug: "experiment-slug",
+ },
+ ],
+ ...overrides,
+ };
+}
+
+
+const getDecorator = (Story) => (
+
+
+
+
+
+);
+
+export const Default = {
+ args: getProfileData(),
+ decorators: [getDecorator],
+};
diff --git a/frontend/src/stories/Rank.stories.jsx b/frontend/src/stories/Rank.stories.jsx
new file mode 100644
index 000000000..840b7e9ed
--- /dev/null
+++ b/frontend/src/stories/Rank.stories.jsx
@@ -0,0 +1,84 @@
+import { BrowserRouter as Router } from "react-router-dom";
+
+import Rank from "../components/Rank/Rank";
+
+export default {
+ title: "Rank",
+ component: Rank,
+ parameters: {
+ layout: "fullscreen",
+ },
+};
+
+function getCupData(overrides = {}) {
+ return {
+ text: "Rank",
+ className: "rank",
+ ...overrides,
+ };
+}
+
+function getScoreData(overrides = {}) {
+ return {
+ score: 100,
+ label: "points",
+ ...overrides,
+ };
+}
+
+function getRankData(cupOverrides = {}, scoreOverrides = {}) {
+ return {
+ cup: getCupData(cupOverrides),
+ score: getScoreData(scoreOverrides),
+ };
+}
+
+const getDecorator = (Story) => (
+
+
+
+
+
+);
+
+export const Default = {
+ args: getRankData(),
+ decorators: [getDecorator],
+};
+
+export const SilverCup = {
+ args: getRankData({ className: "rank silver" }),
+ decorators: [getDecorator],
+};
+
+export const BronzeCup = {
+ args: getRankData({ className: "rank bronze" }),
+ decorators: [getDecorator],
+};
+
+export const NoCupText = {
+ args: getRankData({ text: "" }),
+ decorators: [getDecorator],
+};
+
+export const ZeroScore = {
+ args: getRankData({ score: 0 }),
+ decorators: [getDecorator],
+};
+
+export const NegativeScore = {
+ args: getRankData({ score: -100 }),
+ decorators: [getDecorator],
+};
+
+export const ScoreWithoutLabel = {
+ args: getRankData({ label: "" }),
+ decorators: [getDecorator],
+};
+
+export const CustomLabel = {
+ args: getRankData({ label: "points earned" }),
+ decorators: [getDecorator],
+};
diff --git a/frontend/src/stories/Score.stories.tsx b/frontend/src/stories/Score.stories.tsx
new file mode 100644
index 000000000..a56b561c3
--- /dev/null
+++ b/frontend/src/stories/Score.stories.tsx
@@ -0,0 +1,83 @@
+import { BrowserRouter as Router } from "react-router-dom";
+
+import Score from "../components/Score/Score";
+
+export default {
+ title: "Score",
+ component: Score,
+ parameters: {
+ layout: "fullscreen",
+ },
+ argTypes: {
+ scoreClass: {
+ control: {
+ type: "select",
+ },
+ options: ['diamond', 'platinum', 'gold', 'silver', 'bronze', 'plastic'],
+ }
+ }
+};
+
+function getScoreData(overrides = {}) {
+ return {
+ last_song: "Shania Twain - That don't impress me much",
+ score: 100,
+ score_message: "Correct!",
+ total_score: 200,
+ texts: {
+ score: "Total score",
+ next: "Next",
+ listen_explainer: "You listened to:",
+ },
+ icon: "fa fa-music",
+ feedback: "This is a feedback message",
+ timer: setTimeout(() => { }, 1000),
+ onNext: () => void 0,
+ ...overrides,
+ };
+}
+
+const getDecorator = (Story) => (
+
+
+
+
+
+);
+
+export const Default = {
+ args: getScoreData(),
+ decorators: [getDecorator],
+};
+
+export const ZeroScore = {
+ args: getScoreData({ score: 0, score_message: "No points!" }),
+ decorators: [getDecorator],
+};
+
+export const NegativeScore = {
+ args: getScoreData({ score: -100, score_message: "Incorrect!" }),
+ decorators: [getDecorator],
+};
+
+export const ScoreWithoutLabel = {
+ args: getScoreData({ score_message: "" }),
+ decorators: [getDecorator],
+};
+
+export const CustomLabel = {
+ args: getScoreData({ score_message: "points earned" }),
+ decorators: [getDecorator],
+};
+
+export const CustomScoreClass = {
+ args: getScoreData({ scoreClass: "silver" }),
+ decorators: [getDecorator],
+};
+
+export const SelectableScoreClass = {
+ args: getScoreData(),
+ decorators: [getDecorator],
+};
diff --git a/frontend/src/stories/ScoreCounter.stories.tsx b/frontend/src/stories/ScoreCounter.stories.tsx
new file mode 100644
index 000000000..380a28a9a
--- /dev/null
+++ b/frontend/src/stories/ScoreCounter.stories.tsx
@@ -0,0 +1,54 @@
+import { BrowserRouter as Router } from "react-router-dom";
+
+import ScoreCounter from "../components/ScoreCounter/ScoreCounter";
+
+export default {
+ title: "ScoreCounter",
+ component: ScoreCounter,
+ parameters: {
+ layout: "fullscreen",
+ },
+};
+
+function getScoreData(overrides = {}) {
+ return {
+ score: 100,
+ label: "points",
+ ...overrides,
+ };
+}
+
+const getDecorator = (Story) => (
+
+
+
+
+
+);
+
+export const Default = {
+ args: getScoreData(),
+ decorators: [getDecorator],
+};
+
+export const ZeroScore = {
+ args: getScoreData({ score: 0 }),
+ decorators: [getDecorator],
+};
+
+export const NegativeScore = {
+ args: getScoreData({ score: -100 }),
+ decorators: [getDecorator],
+};
+
+export const ScoreWithoutLabel = {
+ args: getScoreData({ label: "" }),
+ decorators: [getDecorator],
+};
+
+export const CustomLabel = {
+ args: getScoreData({ label: "points earned" }),
+ decorators: [getDecorator],
+};
diff --git a/frontend/src/types/ExperimentCollection.ts b/frontend/src/types/ExperimentCollection.ts
index c78cd736d..dd5f34a27 100644
--- a/frontend/src/types/ExperimentCollection.ts
+++ b/frontend/src/types/ExperimentCollection.ts
@@ -18,5 +18,5 @@ export default interface ExperimentCollection {
aboutContent: string;
consent?: Consent;
theme?: Theme;
- totalScore: Number;
+ totalScore: number;
}
diff --git a/frontend/src/types/Theme.ts b/frontend/src/types/Theme.ts
index cac3db362..b5ae06413 100644
--- a/frontend/src/types/Theme.ts
+++ b/frontend/src/types/Theme.ts
@@ -1,8 +1,15 @@
import Image from "./Image";
+export interface ScoreDisplayConfig {
+ scoreClass: string;
+ scoreLabel: string;
+ noScoreLabel: string;
+}
+
export interface Header {
nextExperimentButtonText: string;
aboutButtonText: string;
+ score: ScoreDisplayConfig;
};
export interface Logo {