diff --git a/backend/experiment/rules/congosamediff.py b/backend/experiment/rules/congosamediff.py index a922899aa..497b8a13f 100644 --- a/backend/experiment/rules/congosamediff.py +++ b/backend/experiment/rules/congosamediff.py @@ -1,4 +1,5 @@ +import random import re import math import string @@ -112,10 +113,10 @@ def next_round(self, session: Session): groups_amount = session.playlist.section_set.values('group').distinct().count() variants_amount = real_trial_variants.count() - # get the participant's group variant - participant_id = session.participant.participant_id_url + # get the participant's group variant based on the participant's id # else default to random number between 1 and variants_amount + participant_id = int(session.participant.participant_id_url) if session.participant.participant_id_url else random.randint(1, variants_amount * 2) participant_group_variant = self.get_participant_group_variant( - int(participant_id), + participant_id, group_number, groups_amount, variants_amount diff --git a/backend/participant/models.py b/backend/participant/models.py index f703847e4..3015e92f6 100644 --- a/backend/participant/models.py +++ b/backend/participant/models.py @@ -42,7 +42,7 @@ def export_admin(self): "participant_id_url": self.participant_id_url, "profile": self.profile_object() } - + def export_profiles(self): # export participant profile result objects return self.result_set.all() diff --git a/backend/participant/views.py b/backend/participant/views.py index f40584cde..d71d34783 100644 --- a/backend/participant/views.py +++ b/backend/participant/views.py @@ -24,6 +24,7 @@ def current(request): 'id': participant.id, 'hash': participant.unique_hash, 'csrf_token': get_token(request), + 'participant_id_url': participant.participant_id_url, 'country': participant.country_code }, json_dumps_params={'indent': 4}) return response diff --git a/frontend/src/API.js b/frontend/src/API.js index c3c9d1bef..bf5e44811 100644 --- a/frontend/src/API.js +++ b/frontend/src/API.js @@ -1,4 +1,4 @@ -import { API_BASE_URL } from "./config"; +import { API_BASE_URL } from "@/config"; import useGet from "./util/useGet"; import axios from "axios"; import qs from "qs"; @@ -15,7 +15,7 @@ export const URLS = { feedback: (slug) => "/experiment/" + slug + "/feedback/", }, experiment_collection: { - get: (slug) => "/experiment/collection/" + slug + "/" + get: (slug) => `/experiment/collection/${slug}/` }, participant: { current: "/participant/", diff --git a/frontend/src/components/App/App.jsx b/frontend/src/components/App/App.jsx deleted file mode 100644 index 716633611..000000000 --- a/frontend/src/components/App/App.jsx +++ /dev/null @@ -1,100 +0,0 @@ -import {useEffect, React} from "react"; -import { - BrowserRouter as Router, - Switch, - Route, - Redirect -} from "react-router-dom"; -import axios from "axios"; - -import { API_BASE_URL, EXPERIMENT_SLUG, URLS } from "../../config.js"; -import { URLS as API_URLS } from "../../API.js"; -import useBoundStore from "../../util/stores.js"; -import Experiment from "../Experiment/Experiment"; -import ExperimentCollection from "../ExperimentCollection/ExperimentCollection"; -import Loading from "../Loading/Loading"; -import Profile from "../Profile/Profile"; -import Reload from "../Reload/Reload"; -import StoreProfile from "../StoreProfile/StoreProfile"; -import useDisableRightClickOnTouchDevices from "../../hooks/useDisableRightClickOnTouchDevices.js"; - - -// App is the root component of our application -const App = () => { - const error = useBoundStore(state => state.error); - const setError = useBoundStore(state => state.setError); - const participant = useBoundStore((state) => state.participant); - const setParticipant = useBoundStore((state) => state.setParticipant); - const queryParams = window.location.search; - - useDisableRightClickOnTouchDevices(); - - useEffect(() => { - const urlParams = new URLSearchParams(queryParams); - const participantId = urlParams.get('participant_id'); - let participantQueryParams = ''; - if (participantId) { - participantQueryParams = `?participant_id=${participantId}`; - } - try { - axios.get(API_BASE_URL + API_URLS.participant.current + participantQueryParams).then(response => { - setParticipant(response.data); - }); - } catch (err) { - console.error(err); - setError('Could not load participant'); - } - }, [setError, queryParams, setParticipant]) - - if (error) { - return

Error: {error}

; - } - - return ( - - { !participant? ( -
- -
- ) : ( - - {/* Request reload for given participant */} - - - - - {/* Default experiment */} - - - - - {/* Profile */} - - - - - {/* Experiment Collection */} - - - {/* Experiment */} - - - - - {/* Store profile */} - - - - - )} -
- ); -}; - -export default App; diff --git a/frontend/src/components/App/App.tsx b/frontend/src/components/App/App.tsx new file mode 100644 index 000000000..9563c413e --- /dev/null +++ b/frontend/src/components/App/App.tsx @@ -0,0 +1,100 @@ +import { useEffect } from "react"; +import { + BrowserRouter as Router, + Switch, + Route, + Redirect +} from "react-router-dom"; +import axios from "axios"; + +import { API_BASE_URL, EXPERIMENT_SLUG, URLS } from "@/config"; +import { URLS as API_URLS } from "../../API"; +import useBoundStore from "../../util/stores"; +import Experiment from "../Experiment/Experiment"; +import ExperimentCollection from "../ExperimentCollection/ExperimentCollection"; +import LoaderContainer from "../LoaderContainer/LoaderContainer"; +import ConditionalRender from "../ConditionalRender/ConditionalRender"; +import Profile from "../Profile/Profile"; +import Reload from "../Reload/Reload"; +import StoreProfile from "../StoreProfile/StoreProfile"; +import useDisableRightClickOnTouchDevices from "../../hooks/useDisableRightClickOnTouchDevices"; + + +// App is the root component of our application +const App = () => { + const error = useBoundStore(state => state.error); + const setError = useBoundStore(state => state.setError); + const participant = useBoundStore((state) => state.participant); + const setParticipant = useBoundStore((state) => state.setParticipant); + const setParticipantLoading = useBoundStore((state) => state.setParticipantLoading); + const queryParams = window.location.search; + + useDisableRightClickOnTouchDevices(); + + useEffect(() => { + const urlParams = new URLSearchParams(queryParams); + const participantId = urlParams.get('participant_id'); + let participantQueryParams = ''; + if (participantId) { + participantQueryParams = `?participant_id=${participantId}`; + } + try { + axios.get(API_BASE_URL + API_URLS.participant.current + participantQueryParams).then(response => { + setParticipant(response.data); + }); + } catch (err) { + console.error(err); + setError('Could not load participant', err); + } finally { + setParticipantLoading(false); + } + }, [setError, queryParams, setParticipant]) + + if (error) { + return

Error: {error}

; + } + + return ( + + }> + + {/* Request reload for given participant */} + + + + + {/* Default experiment */} + + + + + {/* Profile */} + + + + + {/* Experiment Collection */} + + + {/* Experiment */} + + + + + {/* Store profile */} + + + + + + + ); +}; + +export default App; diff --git a/frontend/src/components/AppBar/AppBar.jsx b/frontend/src/components/AppBar/AppBar.jsx index 1589181a5..ae612fca8 100644 --- a/frontend/src/components/AppBar/AppBar.jsx +++ b/frontend/src/components/AppBar/AppBar.jsx @@ -1,5 +1,5 @@ import React from "react"; -import { API_BASE_URL, URLS, LOGO_URL, LOGO_TITLE } from "../../config"; +import { API_BASE_URL, URLS, LOGO_URL, LOGO_TITLE } from "@/config"; import { Link } from "react-router-dom"; import useBoundStore from "@/util/stores"; diff --git a/frontend/src/components/ConditionalRender/ConditionalRender.test.tsx b/frontend/src/components/ConditionalRender/ConditionalRender.test.tsx new file mode 100644 index 000000000..22e85a637 --- /dev/null +++ b/frontend/src/components/ConditionalRender/ConditionalRender.test.tsx @@ -0,0 +1,37 @@ +import ConditionalRender from "./ConditionalRender"; +import { render, screen } from '@testing-library/react'; +import { it, expect, describe } from "vitest"; + +describe("ConditionalRender Component", () => { + it("should render children when condition is true", () => { + render( + fallback}> +
children
+
+ ); + + expect(document.body.contains(screen.getByText("children"))).toBe(true); + expect(document.body.contains(screen.queryByText("fallback"))).toBe(false); + }); + + it("should render fallback when condition is false", () => { + render( + fallback}> +
children
+
+ ); + + expect(document.body.contains(screen.getByText("fallback"))).toBe(true); + expect(document.body.contains(screen.queryByText("children"))).toBe(false); + }); + + it("should render nothing when fallback is not provided and condition is false", () => { + const { container } = render( + +
children
+
+ ); + + expect(container.firstChild).toBeNull(); + }); +}); \ No newline at end of file diff --git a/frontend/src/components/ConditionalRender/ConditionalRender.tsx b/frontend/src/components/ConditionalRender/ConditionalRender.tsx new file mode 100644 index 000000000..d7321bce7 --- /dev/null +++ b/frontend/src/components/ConditionalRender/ConditionalRender.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +interface ConditionalRenderProps { + condition: boolean; + children?: React.ReactNode; + fallback?: React.ReactNode; +} + +const ConditionalRender = ({ condition, children, fallback }: ConditionalRenderProps) => { + if (condition) { + return children; + } + + return fallback || null; +}; + +export default ConditionalRender; diff --git a/frontend/src/components/Consent/Consent.jsx b/frontend/src/components/Consent/Consent.jsx index 255ecfa1c..eb237159b 100644 --- a/frontend/src/components/Consent/Consent.jsx +++ b/frontend/src/components/Consent/Consent.jsx @@ -1,7 +1,7 @@ import React, { useEffect } from "react"; import { saveAs } from 'file-saver'; -import { URLS } from "../../config"; +import { URLS } from "@/config"; import Button from "../Button/Button"; import Loading from "../Loading/Loading"; import { createConsent, useConsent } from "../../API"; diff --git a/frontend/src/components/Experiment/Experiment.jsx b/frontend/src/components/Experiment/Experiment.jsx index 9f65f6ccb..1b1178d85 100644 --- a/frontend/src/components/Experiment/Experiment.jsx +++ b/frontend/src/components/Experiment/Experiment.jsx @@ -72,7 +72,7 @@ const Experiment = ({ match }) => { return newSession; } catch (err) { - setError(`Could not create a session: ${err}`) + setError(`Could not create a session: ${err}`, err) }; }; diff --git a/frontend/src/components/ExperimentCollection/ExperimentCollection.tsx b/frontend/src/components/ExperimentCollection/ExperimentCollection.tsx index 7e7c328d7..41ff414ea 100644 --- a/frontend/src/components/ExperimentCollection/ExperimentCollection.tsx +++ b/frontend/src/components/ExperimentCollection/ExperimentCollection.tsx @@ -6,27 +6,30 @@ import { Switch } from "react-router-dom"; -import useBoundStore from "../../util/stores"; -import { useExperimentCollection } from "../../API"; import Consent from "../Consent/Consent"; import DefaultPage from "../Page/DefaultPage"; +import { useExperimentCollection } from "@/API.js"; import Loading from "../Loading/Loading"; import ExperimentCollectionAbout from "./ExperimentCollectionAbout/ExperimentCollectionAbout"; import ExperimentCollectionDashboard from "./ExperimentCollectionDashboard/ExperimentCollectionDashboard"; -import { URLS } from "../../config"; +import { URLS } from "@/config"; import IExperimentCollection from "@/types/ExperimentCollection"; +import { useBoundStore } from "@/util/stores"; +import { Participant } from "@/types/Participant"; interface RouteParams { slug: string } interface ExperimentCollectionProps extends RouteComponentProps { + participant: Participant } const ExperimentCollection = ({ match }: ExperimentCollectionProps) => { const [experimentCollection, loadingExperimentCollection] = useExperimentCollection(match.params.slug) as [IExperimentCollection, boolean]; const [hasShownConsent, setHasShownConsent] = useState(false); const participant = useBoundStore((state) => state.participant); + const participantIdUrl = participant?.participant_id_url; const nextExperiment = experimentCollection?.next_experiment; const displayDashboard = experimentCollection?.dashboard.length; const showConsent = experimentCollection?.consent; @@ -35,6 +38,8 @@ const ExperimentCollection = ({ match }: ExperimentCollectionProps) => { setHasShownConsent(true); } + const getExperimentHref = (slug: string) => `/${slug}${participantIdUrl ? `?participant_id=${participantIdUrl}` : ""}`; + if (loadingExperimentCollection) { return (
@@ -59,14 +64,14 @@ const ExperimentCollection = ({ match }: ExperimentCollectionProps) => { } if (!displayDashboard && nextExperiment) { - return ; + return } return (
} /> - } /> + } />
) diff --git a/frontend/src/components/ExperimentCollection/ExperimentCollectionDashboard/ExperimentCollectionDashboard.test.tsx b/frontend/src/components/ExperimentCollection/ExperimentCollectionDashboard/ExperimentCollectionDashboard.test.tsx index 46f33adf6..b0edad3b0 100644 --- a/frontend/src/components/ExperimentCollection/ExperimentCollectionDashboard/ExperimentCollectionDashboard.test.tsx +++ b/frontend/src/components/ExperimentCollection/ExperimentCollectionDashboard/ExperimentCollectionDashboard.test.tsx @@ -7,8 +7,11 @@ import Experiment from '@/types/Experiment'; const getExperiment = (overrides = {}) => { return { + id: 1, slug: 'some_slug', name: 'Some Experiment', + description: 'Some description', + image: '', started_session_count: 2, finished_session_count: 1, ...overrides @@ -16,17 +19,19 @@ const getExperiment = (overrides = {}) => { } const experiment1 = getExperiment({ + id: 1, slug: 'some_slug', - name: 'Some Experiment' + name: 'Some Experiment', + description: null, }); const experiment2 = getExperiment({ + id: 2, slug: 'another_slug', name: 'Another Experiment', - finished_session_count: 2 + finished_session_count: 2, + description: 'Some description', }); -const experimentWithAllProps = getExperiment({ image: 'some_image.jpg', description: 'Some description' }); - describe('ExperimentCollectionDashboard', () => { it('shows a dashboard of multiple experiments if it receives an array', async () => { @@ -43,4 +48,40 @@ describe('ExperimentCollectionDashboard', () => { expect(counters[1].innerHTML).toBe(experiment1.finished_session_count.toString()); }) }); + + it('shows a placeholder if an experiment has no image', async () => { + render( + + + + ); + await waitFor(() => { + expect(screen.getByRole('menu')).toBeTruthy(); + expect(screen.getByRole('menu').querySelector('.placeholder')).toBeTruthy(); + }); + }); + + it('links to the experiment with the correct slug', async () => { + render( + + + + ); + await waitFor(() => { + expect(screen.getByRole('menu')).toBeTruthy(); + expect(screen.getByRole('menu').querySelector('a').getAttribute('href')).toBe('/some_slug'); + }); + }); + + it('links to the experiment with the correct slug and participant id if the participand id url is present', async () => { + render( + + + + ); + await waitFor(() => { + expect(screen.getByRole('menu')).toBeTruthy(); + expect(screen.getByRole('menu').querySelector('a').getAttribute('href')).toBe('/some_slug?participant_id=some_id'); + }); + }); }) \ No newline at end of file diff --git a/frontend/src/components/ExperimentCollection/ExperimentCollectionDashboard/ExperimentCollectionDashboard.tsx b/frontend/src/components/ExperimentCollection/ExperimentCollectionDashboard/ExperimentCollectionDashboard.tsx index c2205e9b2..9df7ef057 100644 --- a/frontend/src/components/ExperimentCollection/ExperimentCollectionDashboard/ExperimentCollectionDashboard.tsx +++ b/frontend/src/components/ExperimentCollection/ExperimentCollectionDashboard/ExperimentCollectionDashboard.tsx @@ -1,15 +1,16 @@ import React from "react"; import { Link } from "react-router-dom"; -import { API_ROOT } from "../../../config"; +import { API_ROOT } from "@/config"; import ExperimentCollection from "@/types/ExperimentCollection"; interface ExperimentCollectionDashboardProps { experimentCollection: ExperimentCollection; + participantIdUrl: string | null; } -export const ExperimentCollectionDashboard: React.FC = ({ experimentCollection }) => { +export const ExperimentCollectionDashboard: React.FC = ({ experimentCollection, participantIdUrl }) => { const dashboard = experimentCollection?.dashboard; @@ -17,6 +18,8 @@ export const ExperimentCollectionDashboard: React.FC `/${slug}${participantIdUrl ? `?participant_id=${participantIdUrl}` : ""}`; + return ( <>
@@ -36,7 +39,7 @@ export const ExperimentCollectionDashboard: React.FC {dashboard.map((exp) => (
  • - +

    {exp.name}

    diff --git a/frontend/src/components/Final/Final.jsx b/frontend/src/components/Final/Final.jsx index 9e5c0ec63..7c5a27b1c 100644 --- a/frontend/src/components/Final/Final.jsx +++ b/frontend/src/components/Final/Final.jsx @@ -4,7 +4,7 @@ import { withRouter } from "react-router-dom"; import Rank from "../Rank/Rank"; import Social from "../Social/Social"; -import { URLS } from "../../config"; +import { URLS } from "@/config"; import { finalizeSession } from "../../API"; import useBoundStore from "../../util/stores"; import ParticipantLink from "../ParticipantLink/ParticipantLink"; diff --git a/frontend/src/components/LoaderContainer/LoaderContainer.jsx b/frontend/src/components/LoaderContainer/LoaderContainer.jsx new file mode 100644 index 000000000..cfc0c3985 --- /dev/null +++ b/frontend/src/components/LoaderContainer/LoaderContainer.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import Loading from '@/components/Loading/Loading'; + +const LoadingContainer = () => ( +
    + +
    +) + +export default LoadingContainer; diff --git a/frontend/src/components/MatchingPairs/PlayCard.test.jsx b/frontend/src/components/MatchingPairs/PlayCard.test.jsx index ea35b12af..46af6b8cc 100644 --- a/frontend/src/components/MatchingPairs/PlayCard.test.jsx +++ b/frontend/src/components/MatchingPairs/PlayCard.test.jsx @@ -62,7 +62,6 @@ describe("PlayCard Component Tests", () => { it("should display a card with fbmemory when memory", () => { render(); - const check = screen.getByRole("button").classList; expect(screen.getByRole("button").classList.contains("fbmemory")).to.be.true; }); diff --git a/frontend/src/components/PlayButton/PlayButton.jsx b/frontend/src/components/PlayButton/PlayButton.jsx index 4c046cd18..0bd576152 100644 --- a/frontend/src/components/PlayButton/PlayButton.jsx +++ b/frontend/src/components/PlayButton/PlayButton.jsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React from "react"; import classNames from "classnames"; const PlayButton = ({ playSection, isPlaying, className = "", disabled }) => { diff --git a/frontend/src/components/Preload/Preload.jsx b/frontend/src/components/Preload/Preload.jsx index ed835c185..baa3b8c54 100644 --- a/frontend/src/components/Preload/Preload.jsx +++ b/frontend/src/components/Preload/Preload.jsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react"; import classNames from "classnames"; -import { MEDIA_ROOT } from "../../config"; +import { MEDIA_ROOT } from "@/config"; import ListenFeedback from "../Listen/ListenFeedback"; import CountDown from "../CountDown/CountDown"; import * as audio from "../../util/audio"; diff --git a/frontend/src/components/Profile/Profile.jsx b/frontend/src/components/Profile/Profile.jsx index 86d4b64a1..8fc492d62 100644 --- a/frontend/src/components/Profile/Profile.jsx +++ b/frontend/src/components/Profile/Profile.jsx @@ -4,7 +4,7 @@ import DefaultPage from "../Page/DefaultPage"; import Loading from "../Loading/Loading"; import Rank from "../Rank/Rank"; import { useParticipantScores } from "../../API"; -import { URLS } from "../../config"; +import { URLS } from "@/config"; import ParticipantLink from "../ParticipantLink/ParticipantLink"; // Profile loads and shows the profile of a participant for a given experiment diff --git a/frontend/src/components/Reload/Reload.jsx b/frontend/src/components/Reload/Reload.jsx index 947d314f1..21792123c 100644 --- a/frontend/src/components/Reload/Reload.jsx +++ b/frontend/src/components/Reload/Reload.jsx @@ -1,7 +1,7 @@ import React, { useEffect } from "react"; import { useLocation } from 'react-router-dom'; -import { API_BASE_URL } from "../../config"; +import { API_BASE_URL } from "@/config"; const Reload = () => { const location = useLocation(); diff --git a/frontend/src/components/StoreProfile/StoreProfile.jsx b/frontend/src/components/StoreProfile/StoreProfile.jsx index 3c0297b76..cd1be0eae 100644 --- a/frontend/src/components/StoreProfile/StoreProfile.jsx +++ b/frontend/src/components/StoreProfile/StoreProfile.jsx @@ -2,7 +2,7 @@ import React, { useState } from "react"; import classNames from "classnames"; import { Link, withRouter } from "react-router-dom"; import * as EmailValidator from "email-validator"; -import { URLS } from "../../config"; +import { URLS } from "@/config"; import useBoundStore from "../../util/stores"; import { shareParticipant} from "../../API"; import DefaultPage from "../Page/DefaultPage"; diff --git a/frontend/src/components/ToontjeHoger/ToontjeHoger.jsx b/frontend/src/components/ToontjeHoger/ToontjeHoger.jsx index 93a590990..707b860ec 100644 --- a/frontend/src/components/ToontjeHoger/ToontjeHoger.jsx +++ b/frontend/src/components/ToontjeHoger/ToontjeHoger.jsx @@ -1,5 +1,5 @@ import React, { useEffect, useState, useRef } from "react"; -import { LOGO_TITLE } from "../../config"; +import { LOGO_TITLE } from "@/config"; import { Switch, Route, Link } from "react-router-dom"; import Rank from "../Rank/Rank"; diff --git a/frontend/src/config.js b/frontend/src/config.ts similarity index 71% rename from frontend/src/config.js rename to frontend/src/config.ts index e32b8f93f..2c4185ad6 100644 --- a/frontend/src/config.js +++ b/frontend/src/config.ts @@ -1,5 +1,5 @@ // Load experiment slug from hash, or default to env experiment slug -export const EXPERIMENT_SLUG = +export const EXPERIMENT_SLUG: string = document.location.hash.indexOf("slug=") > -1 ? document.location.hash.split("slug=")[1] : import.meta.env.VITE_EXPERIMENT_SLUG; @@ -9,7 +9,7 @@ export const EXPERIMENT_SLUG = // Make sure your app url is set in the CORS_ORIGIN_WHITELIST in // the API's base_settings.py -export const API_ROOT = import.meta.env.VITE_API_ROOT; +export const API_ROOT: string = import.meta.env.VITE_API_ROOT; export const API_BASE_URL = API_ROOT; // Media @@ -17,8 +17,8 @@ export const MEDIA_ROOT = API_ROOT; export const SILENT_MP3 = "/audio/silent.mp3"; // Logo -export const LOGO_URL = import.meta.env.VITE_LOGO_URL || '/images/logo-white.svg'; -export const LOGO_TITLE = import.meta.env.VITE_HTML_PAGE_TITLE || 'Amsterdam Music Lab'; +export const LOGO_URL: string = import.meta.env.VITE_LOGO_URL || '/images/logo-white.svg'; +export const LOGO_TITLE: string = import.meta.env.VITE_HTML_PAGE_TITLE || 'Amsterdam Music Lab'; // Background export const BACKGROUND_URL = import.meta.env.VITE_BACKGROUND_URL || '/images/background.jpg' @@ -34,5 +34,5 @@ export const URLS = { experimentCollection: "/collection/:slug", reloadParticipant: "/participant/reload/:id/:hash", AMLHome: - import.meta.env.VITE_AML_HOME || "https://www.amsterdammusiclab.nl", + import.meta.env.VITE_AML_HOME as string || "https://www.amsterdammusiclab.nl", }; diff --git a/frontend/src/hooks/useResultHandler.js b/frontend/src/hooks/useResultHandler.js index fa6463fcc..4d99af38e 100644 --- a/frontend/src/hooks/useResultHandler.js +++ b/frontend/src/hooks/useResultHandler.js @@ -1,5 +1,5 @@ import { useRef, useCallback } from "react"; -import { scoreResult } from "../API.js"; +import { scoreResult } from "../API"; // useResult provides a reusable function to handle experiment view data // - collect results in a buffer diff --git a/frontend/src/hooks/useResultHandler.test.js b/frontend/src/hooks/useResultHandler.test.js index 7c3e0667e..21622bb22 100644 --- a/frontend/src/hooks/useResultHandler.test.js +++ b/frontend/src/hooks/useResultHandler.test.js @@ -2,9 +2,9 @@ import { renderHook, act } from "@testing-library/react"; import useResultHandler from "./useResultHandler"; import { vi } from 'vitest'; -import * as API from '../API.js'; +import * as API from '../API'; -vi.mock('../API.js'); +vi.mock('../API'); describe('useResultHandler', () => { diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 62f0ad80f..d39a91f9b 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -1,7 +1,7 @@ import "./index.scss"; import { StrictMode } from "react"; import { createRoot } from 'react-dom/client'; -import App from "./components/App/App"; +import App from "./components/App/App.tsx"; import { initSentry } from "./config/sentry"; import { initAudioListener } from "./util/audio"; import { initWebAudioListener } from "./util/webAudio"; diff --git a/frontend/src/types/Participant.ts b/frontend/src/types/Participant.ts new file mode 100644 index 000000000..c3f596213 --- /dev/null +++ b/frontend/src/types/Participant.ts @@ -0,0 +1,7 @@ +export interface Participant { + id: number; + hash: string; + csrf_token: string; + participant_id_url: string; + country: string; +} \ No newline at end of file diff --git a/frontend/src/util/audio.js b/frontend/src/util/audio.js index 16dcf2e15..491416d45 100644 --- a/frontend/src/util/audio.js +++ b/frontend/src/util/audio.js @@ -1,4 +1,4 @@ -import { API_ROOT, MEDIA_ROOT, SILENT_MP3 } from "../config.js"; +import { API_ROOT, MEDIA_ROOT, SILENT_MP3 } from "../config"; import Timer from "./timer"; // Audio provides function around a shared audio object diff --git a/frontend/src/util/stores.js b/frontend/src/util/stores.js deleted file mode 100644 index dbd78552d..000000000 --- a/frontend/src/util/stores.js +++ /dev/null @@ -1,30 +0,0 @@ -import { create } from "zustand"; - -const createErrorSlice = (set) => ({ - error: null, - setError: (error) => set(() => ({ error })) -}); - -const createParticipantSlice = (set) => ({ - participant: null, - setParticipant: (participant) => set(() => ({ participant })) -}); - -const createSessionSlice = (set) => ({ - session: null, - setSession: (session) => set(() => ({ session })) -}); - -const createThemeSlice = (set) => ({ - theme: null, - setTheme: (theme) => set(() => ({ theme })), -}); - -export const useBoundStore = create((...args) => ({ - ...createErrorSlice(...args), - ...createParticipantSlice(...args), - ...createSessionSlice(...args), - ...createThemeSlice(...args), -})); - -export default useBoundStore; \ No newline at end of file diff --git a/frontend/src/util/stores.ts b/frontend/src/util/stores.ts new file mode 100644 index 000000000..154f9558c --- /dev/null +++ b/frontend/src/util/stores.ts @@ -0,0 +1,61 @@ +import { create, StateCreator } from "zustand"; +import * as Sentry from '@sentry/react'; +import { Participant } from "@/types/Participant"; + +interface ErrorSlice { + error: string | null; + setError: (message: string, errorToCapture?: Error) => void; +} + +const createErrorSlice: StateCreator = (set) => ({ + error: null, + setError: (message, errorToCapture) => { + set(() => ({ error: message })); + if (errorToCapture) { + Sentry.captureException(errorToCapture); + } + } +}); + +interface ParticipantSlice { + participant: Participant | null; + participantLoading: boolean; + setParticipant: (participant: Participant) => void; + setParticipantLoading: (participantLoading: boolean) => void; +} + +const createParticipantSlice: StateCreator = (set) => ({ + participant: null, + participantLoading: true, + setParticipant: (participant) => set(() => ({ participant })), + setParticipantLoading: (participantLoading) => set(() => ({ participantLoading })) +}); + +interface SessionSlice { + session: string | null; + setSession: (session: string) => void; +} + +const createSessionSlice: StateCreator = (set) => ({ + session: null, + setSession: (session) => set(() => ({ session })) +}); + +interface ThemeSlice { + theme: string | null; + setTheme: (theme: string) => void; +} + +const createThemeSlice: StateCreator = (set) => ({ + theme: null, + setTheme: (theme) => set(() => ({ theme })), +}); + +export const useBoundStore = create((...args) => ({ + ...createErrorSlice(...args), + ...createParticipantSlice(...args), + ...createSessionSlice(...args), + ...createThemeSlice(...args), +})); + +export default useBoundStore; \ No newline at end of file diff --git a/frontend/src/util/webAudio.js b/frontend/src/util/webAudio.js index f98b3e873..dd0d41c9a 100644 --- a/frontend/src/util/webAudio.js +++ b/frontend/src/util/webAudio.js @@ -1,4 +1,4 @@ -import { MEDIA_ROOT } from "../config"; +import { MEDIA_ROOT } from "@/config"; let track; let source;