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;