From 4de65d7fa4ed0e1ae41cf637712b91f819126969 Mon Sep 17 00:00:00 2001
From: Anyul Rivas
Date: Mon, 24 Mar 2025 23:00:16 +0100
Subject: [PATCH 01/11] refactor: remove duplication on fetch speaker logic
---
.../SpeakersCarousel/SpeakerSwiper.tsx | 4 +-
.../SpeakerDetailContainer2023.tsx | 4 +-
src/2023/Speakers/Speakers2023.tsx | 4 +-
src/2023/Speakers/UseFetchSpeakers.ts | 22 --
.../TalkDetail/TalkDetailContainer2023.tsx | 4 +-
.../SpeakerDetailContainer2024.tsx | 4 +-
src/2024/Speakers/Speakers2024.tsx | 4 +-
src/2024/Speakers/UseFetchSpeakers.test.tsx | 144 ----------
src/2024/Speakers/UseFetchSpeakers.ts | 22 --
src/2024/SpeakersCarousel/SpeakerSwiper.tsx | 14 +-
.../TalkDetail/MeetingDetailContainer.tsx | 4 +-
src/components/Swiper/SpeakerSwiper.tsx | 2 +-
src/hooks/useFetchSpeakers.test.tsx | 257 ++++++++++++++++++
src/hooks/useFetchSpeakers.ts | 45 +++
.../MeetingDetail/TalkDetailContainer2024.tsx | 2 +-
.../SpeakerDetail/SpeakerDetailContainer.tsx | 2 +-
src/views/Speakers/Speakers.tsx | 2 +-
src/views/Speakers/UseFetchSpeakers.test.tsx | 144 ----------
src/views/Speakers/UseFetchSpeakers.ts | 22 --
19 files changed, 327 insertions(+), 379 deletions(-)
delete mode 100644 src/2023/Speakers/UseFetchSpeakers.ts
delete mode 100644 src/2024/Speakers/UseFetchSpeakers.test.tsx
delete mode 100644 src/2024/Speakers/UseFetchSpeakers.ts
create mode 100644 src/hooks/useFetchSpeakers.test.tsx
create mode 100644 src/hooks/useFetchSpeakers.ts
delete mode 100644 src/views/Speakers/UseFetchSpeakers.test.tsx
delete mode 100644 src/views/Speakers/UseFetchSpeakers.ts
diff --git a/src/2023/Home/components/SpeakersCarousel/SpeakerSwiper.tsx b/src/2023/Home/components/SpeakersCarousel/SpeakerSwiper.tsx
index 428ac669c..829c8220d 100644
--- a/src/2023/Home/components/SpeakersCarousel/SpeakerSwiper.tsx
+++ b/src/2023/Home/components/SpeakersCarousel/SpeakerSwiper.tsx
@@ -7,7 +7,7 @@ import "swiper/swiper-bundle.min.css";
import "./SpeakersCarousel.scss";
import { Link } from "react-router";
import { ROUTE_SPEAKER_DETAIL } from "../../../../constants/routes";
-import { useFetchSpeakers } from "../../../Speakers/UseFetchSpeakers";
+import { useFetchSpeakers } from "../../../../hooks/useFetchSpeakers";
import * as Sentry from "@sentry/react";
const StyledSlideImage = styled.img`
@@ -35,7 +35,7 @@ const StyledSlideText = styled.p`
color: white;
`;
const SpeakerSwiper: FC> = () => {
- const { isLoading, data, error } = useFetchSpeakers();
+ const { isLoading, data, error } = useFetchSpeakers("2023");
const swiperSpeakers = data?.sort(() => 0.5 - Math.random()).slice(0, 20);
diff --git a/src/2023/SpeakerDetail/SpeakerDetailContainer2023.tsx b/src/2023/SpeakerDetail/SpeakerDetailContainer2023.tsx
index cdf001c87..db5df3855 100644
--- a/src/2023/SpeakerDetail/SpeakerDetailContainer2023.tsx
+++ b/src/2023/SpeakerDetail/SpeakerDetailContainer2023.tsx
@@ -6,13 +6,13 @@ import SpeakerDetail2023 from "./SpeakerDetail2023";
import { useParams } from "react-router";
import { StyledContainer, StyledWaveContainer } from "./Speaker.style";
import conferenceData from "../../data/2023.json";
-import { useFetchSpeakers } from "../Speakers/UseFetchSpeakers";
+import { useFetchSpeakers } from "../../hooks/useFetchSpeakers";
import * as Sentry from "@sentry/react";
const SpeakerDetailContainer2023: FC> = () => {
const { id } = useParams<{ id: string }>();
- const { isLoading, error, data } = useFetchSpeakers(id);
+ const { isLoading, error, data } = useFetchSpeakers("2023", id);
if (error) {
Sentry.captureException(error);
diff --git a/src/2023/Speakers/Speakers2023.tsx b/src/2023/Speakers/Speakers2023.tsx
index 5fd28c263..1d3ed6d4d 100644
--- a/src/2023/Speakers/Speakers2023.tsx
+++ b/src/2023/Speakers/Speakers2023.tsx
@@ -20,7 +20,7 @@ import {
import webData from "../../data/2023.json";
import Button from "../../components/UI/Button";
import {gaEventTracker} from "../../components/analytics/Analytics";
-import {useFetchSpeakers} from "./UseFetchSpeakers";
+import {useFetchSpeakers} from "../../hooks/useFetchSpeakers";
import * as Sentry from "@sentry/react";
import {ISpeaker} from "../../types/speakers";
@@ -41,7 +41,7 @@ const Speakers2023: FC> = () => {
const isBetween = (startDay: Date, endDay: Date): boolean =>
startDay < new Date() && endDay > today;
- const { error, data, isLoading } = useFetchSpeakers();
+ const { error, data, isLoading } = useFetchSpeakers("2023");
if (error) {
Sentry.captureException(error);
diff --git a/src/2023/Speakers/UseFetchSpeakers.ts b/src/2023/Speakers/UseFetchSpeakers.ts
deleted file mode 100644
index d0727ee74..000000000
--- a/src/2023/Speakers/UseFetchSpeakers.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import {useQuery, UseQueryResult} from "react-query";
-import axios from "axios";
-import {speakerAdapter} from "../../services/speakerAdapter";
-import {ISpeaker} from "../../types/speakers";
-
-export const useFetchSpeakers = (id?: string): UseQueryResult => {
- return useQuery("api-speakers", async () => {
- const serverResponse = await axios.get(
- "https://sessionize.com/api/v2/ttsitynd/view/Speakers"
- );
- let returnData;
- if (id !== undefined) {
- returnData = serverResponse.data.filter(
- (speaker: { id: string }) => speaker.id === id
- );
- } else {
- returnData = serverResponse.data;
- }
- return speakerAdapter(returnData);
- });
-};
-
diff --git a/src/2023/TalkDetail/TalkDetailContainer2023.tsx b/src/2023/TalkDetail/TalkDetailContainer2023.tsx
index f1f096ea3..80b6fd597 100644
--- a/src/2023/TalkDetail/TalkDetailContainer2023.tsx
+++ b/src/2023/TalkDetail/TalkDetailContainer2023.tsx
@@ -7,7 +7,7 @@ import {useParams} from "react-router";
import conferenceData from "../../data/2023.json";
import {useFetchTalksById} from "../Talks/UseFetchTalks";
import * as Sentry from "@sentry/react";
-import {useFetchSpeakers} from "../Speakers/UseFetchSpeakers";
+import {useFetchSpeakers} from "../../hooks/useFetchSpeakers";
import {Session} from "../../types/sessions";
import TalkDetail from "./TalkDetail";
import {ISpeaker} from "../../types/speakers";
@@ -19,7 +19,7 @@ const StyledContainer = styled.div`
const TalkDetailContainer2023: FC> = () => {
const { id } = useParams<{ id: string }>();
const { isLoading, error, data } = useFetchTalksById(id!);
- const { data: speakerData } = useFetchSpeakers();
+ const { data: speakerData } = useFetchSpeakers("2023");
const getTalkSpeakers = (
data: Session[] | undefined,
diff --git a/src/2024/SpeakerDetail/SpeakerDetailContainer2024.tsx b/src/2024/SpeakerDetail/SpeakerDetailContainer2024.tsx
index 8d7be2b88..8f3b838cd 100644
--- a/src/2024/SpeakerDetail/SpeakerDetailContainer2024.tsx
+++ b/src/2024/SpeakerDetail/SpeakerDetailContainer2024.tsx
@@ -5,7 +5,7 @@ import SectionWrapper from "../../components/SectionWrapper/SectionWrapper";
import SpeakerDetail from "./SpeakerDetail";
import {useParams} from "react-router";
import conferenceData from "../../data/2024.json";
-import {useFetchSpeakers} from "../Speakers/UseFetchSpeakers";
+import {useFetchSpeakers} from "../../hooks/useFetchSpeakers";
import * as Sentry from "@sentry/react";
import {StyledContainer} from "../../views/SpeakerDetail/Speaker.style";
import {StyledWaveContainer} from "../../views/Talks/Talks.style";
@@ -13,7 +13,7 @@ import {StyledWaveContainer} from "../../views/Talks/Talks.style";
const SpeakerDetailContainer2024: FC> = () => {
const {id} = useParams<{ id: string }>();
- const {isLoading, error, data} = useFetchSpeakers(id);
+ const {isLoading, error, data} = useFetchSpeakers("2024", id);
if (error) {
Sentry.captureException(error);
diff --git a/src/2024/Speakers/Speakers2024.tsx b/src/2024/Speakers/Speakers2024.tsx
index c5f4e9730..1b978018e 100644
--- a/src/2024/Speakers/Speakers2024.tsx
+++ b/src/2024/Speakers/Speakers2024.tsx
@@ -19,7 +19,7 @@ import {
import webData from "../../data/2024.json";
import Button from "../../components/UI/Button";
import {gaEventTracker} from "../../components/analytics/Analytics";
-import {useFetchSpeakers} from "./UseFetchSpeakers";
+import {useFetchSpeakers} from "../../hooks/useFetchSpeakers";
import * as Sentry from "@sentry/react";
import {SpeakerCard} from "../../views/Speakers/components/SpeakersCard";
import {ISpeaker} from "../../types/speakers";
@@ -41,7 +41,7 @@ const Speakers2024: FC> = () => {
const isBetween = (startDay: Date, endDay: Date): boolean =>
startDay < new Date() && endDay > today;
- const {error, data, isLoading} = useFetchSpeakers();
+ const {error, data, isLoading} = useFetchSpeakers("2024");
if (error) {
Sentry.captureException(error);
diff --git a/src/2024/Speakers/UseFetchSpeakers.test.tsx b/src/2024/Speakers/UseFetchSpeakers.test.tsx
deleted file mode 100644
index 2adad88fc..000000000
--- a/src/2024/Speakers/UseFetchSpeakers.test.tsx
+++ /dev/null
@@ -1,144 +0,0 @@
-import React, {FC} from "react";
-import {QueryClient, QueryClientProvider} from "react-query";
-import {renderHook, waitFor} from "@testing-library/react";
-import {useFetchSpeakers} from "./UseFetchSpeakers";
-import axios, {AxiosHeaders, AxiosResponse} from "axios";
-import {speakerAdapter} from "../../services/speakerAdapter";
-import {IResponse} from "../../types/speakers";
-
-jest.mock("axios");
-const mockedAxios = axios as jest.Mocked;
-const axiosHeaders = new AxiosHeaders();
-
-const payload: AxiosResponse = {
- status: 200,
- statusText: "OK",
- headers: {},
- config: {
- headers: axiosHeaders,
- },
- data: [
- {
- id: "1",
- fullName: "John Smith",
- profilePicture: "https://example.com/john.jpg",
- tagLine: "Software engineer",
- bio: "I am a software engineer",
- sessions: [
- {
- id: 4567,
- name: "sample session",
- },
- ],
- links: [
- {
- linkType: "Twitter",
- url: "https://twitter.com/johnsmith",
- title: "",
- },
- {
- linkType: "LinkedIn",
- url: "https://linkedin.com/in/johnsmith",
- title: "",
- },
- ],
- },
- {
- id: "2",
- fullName: "Jane Doe",
- profilePicture: "https://example.com/jane.jpg",
- tagLine: "Data scientist",
- bio: "I am a data scientist",
- sessions: [],
- links: [
- {
- linkType: "Twitter",
- url: "https://twitter.com/janedoe",
- title: "",
- },
- {
- linkType: "LinkedIn",
- url: "https://linkedin.com/in/janedoe",
- title: "",
- },
- ],
- },
- ],
-};
-
-describe("fetch speaker hook and speaker adapter", () => {
- beforeAll(() => {
- jest.mock("axios");
- });
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
- it("should adapt from a server response", async () => {
- const queryClient = new QueryClient();
-
- mockedAxios.get.mockImplementation(() => Promise.resolve(payload));
- const wrapper: FC>> = ({children}) => {
- return (
-
- {children}
-
- );
- };
-
- const {result} = renderHook(() => useFetchSpeakers(), {
- wrapper,
- });
- await waitFor(() => result.current.isSuccess, {});
- await waitFor(() => !result.current.isLoading, {});
- expect(mockedAxios.get).toHaveBeenCalled();
- expect(result.current.isLoading).toEqual(false);
- expect(result.current.error).toEqual(null);
- expect(result.current.data).toEqual(speakerAdapter(payload.data));
- });
-
- it("should adapt from server response a query with id", async () => {
- //Given
- const queryClient = new QueryClient();
- mockedAxios.get.mockResolvedValueOnce(payload);
- const expectedPayload: IResponse[] = [
- {
- id: "1",
- bio: "I am a software engineer",
- fullName: "John Smith",
- links: [
- {
- linkType: "LinkedIn",
- url: "https://linkedin.com/in/johnsmith",
- title: "",
- },
- {
- url: "https://twitter.com/johnsmith",
- title: "",
- linkType: "Twitter",
- },
- ],
- profilePicture: "https://example.com/john.jpg",
- tagLine: "Software engineer",
- sessions: [{id: 4567, name: "sample session"}],
- },
- ];
- const wrapper: FC>> = ({children}) => {
- return (
-
- {children}
-
- );
- };
-
- //When
- const {result} = renderHook(() => useFetchSpeakers("1"), {
- wrapper,
- });
- await waitFor(() => result.current.isSuccess);
- await waitFor(() => !result.current.isLoading, {});
- //then
- expect(mockedAxios.get).toHaveBeenCalled();
- expect(result.current.data).toEqual(speakerAdapter(expectedPayload));
- });
-});
diff --git a/src/2024/Speakers/UseFetchSpeakers.ts b/src/2024/Speakers/UseFetchSpeakers.ts
deleted file mode 100644
index 40c738bee..000000000
--- a/src/2024/Speakers/UseFetchSpeakers.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import {useQuery, UseQueryResult} from "react-query";
-import axios from "axios";
-import {speakerAdapter} from "../../services/speakerAdapter";
-import {ISpeaker} from "../../types/speakers";
-
-export const useFetchSpeakers = (id?: string): UseQueryResult => {
- return useQuery("api-speakers", async () => {
- const serverResponse = await axios.get(
- "https://sessionize.com/api/v2/teq4asez/view/Speakers",
- );
- let returnData;
- if (id !== undefined) {
- returnData = serverResponse.data.filter(
- (speaker: { id: string }) => speaker.id === id,
- );
- } else {
- returnData = serverResponse.data;
- }
- return speakerAdapter(returnData);
- });
-};
-
diff --git a/src/2024/SpeakersCarousel/SpeakerSwiper.tsx b/src/2024/SpeakersCarousel/SpeakerSwiper.tsx
index 7231db567..03f1a731a 100644
--- a/src/2024/SpeakersCarousel/SpeakerSwiper.tsx
+++ b/src/2024/SpeakersCarousel/SpeakerSwiper.tsx
@@ -6,7 +6,7 @@ import "swiper/swiper-bundle.min.css";
import "./SpeakersCarousel.scss";
import {Link} from "react-router";
import conferenceData from "../../data/2024.json";
-import {useFetchSpeakers} from "../Speakers/UseFetchSpeakers";
+import {useFetchSpeakers} from "../../hooks/useFetchSpeakers";
import * as Sentry from "@sentry/react";
import {Color} from "../../styles/colors";
import {ROUTE_SPEAKER_DETAIL} from "../../constants/routes";
@@ -36,28 +36,28 @@ const StyledSlideText = styled.p`
color: white;
`;
const SpeakerSwiper: FC> = () => {
- const {isLoading, data, error} = useFetchSpeakers();
+ const {isLoading, data, error} = useFetchSpeakers("2024");
// Securely shuffle the speakers using Fisher-Yates algorithm with crypto API
const swiperSpeakers = React.useMemo(() => {
if (!data) return null;
-
+
// Create a copy of the data to avoid mutating the original
const speakersCopy = [...data];
-
+
// Fisher-Yates shuffle with crypto.getRandomValues for secure randomization
for (let i = speakersCopy.length - 1; i > 0; i--) {
// Generate a secure random value using crypto API
const randomBuffer = new Uint32Array(1);
window.crypto.getRandomValues(randomBuffer);
-
+
// Use the random value to get an index between 0 and i (inclusive)
const j = randomBuffer[0] % (i + 1);
-
+
// Swap elements at i and j
[speakersCopy[i], speakersCopy[j]] = [speakersCopy[j], speakersCopy[i]];
}
-
+
// Return the first 20 speakers from the shuffled array
return speakersCopy.slice(0, 20);
}, [data]);
diff --git a/src/2024/TalkDetail/MeetingDetailContainer.tsx b/src/2024/TalkDetail/MeetingDetailContainer.tsx
index f48834d93..ef30c0d69 100644
--- a/src/2024/TalkDetail/MeetingDetailContainer.tsx
+++ b/src/2024/TalkDetail/MeetingDetailContainer.tsx
@@ -7,7 +7,7 @@ import {useParams} from "react-router";
import conferenceData from "../../data/2024.json";
import {useFetchTalksById} from "../Talks/UseFetchTalks";
import * as Sentry from "@sentry/react";
-import {useFetchSpeakers} from "../Speakers/UseFetchSpeakers";
+import {useFetchSpeakers} from "../../hooks/useFetchSpeakers";
import MeetingDetail from "./MeetingDetail";
import {ISpeaker} from "../../types/speakers";
@@ -20,7 +20,7 @@ const StyledContainer = styled.div`
const MeetingDetailContainer: FC> = () => {
const {id} = useParams<{ id: string }>();
const {isLoading, error, data} = useFetchTalksById(id!);
- const {data: speakerData} = useFetchSpeakers();
+ const {data: speakerData} = useFetchSpeakers("2024");
const getTalkSpeakers = (
data: Session[] | undefined,
diff --git a/src/components/Swiper/SpeakerSwiper.tsx b/src/components/Swiper/SpeakerSwiper.tsx
index f22ff0779..13316dab2 100644
--- a/src/components/Swiper/SpeakerSwiper.tsx
+++ b/src/components/Swiper/SpeakerSwiper.tsx
@@ -6,7 +6,7 @@ import {Color} from "../../styles/colors";
import "swiper/swiper-bundle.min.css";
import "../../views/Home/components/SpeakersCarousel/SpeakersCarousel.scss";
import conferenceData from "../../data/2025.json";
-import {useFetchSpeakers} from "../../views/Speakers/UseFetchSpeakers";
+import {useFetchSpeakers} from "../../hooks/useFetchSpeakers";
import * as Sentry from "@sentry/react";
import {ISpeaker} from "../../types/speakers";
import {ROUTE_SPEAKER_DETAIL} from "../../constants/routes";
diff --git a/src/hooks/useFetchSpeakers.test.tsx b/src/hooks/useFetchSpeakers.test.tsx
new file mode 100644
index 000000000..b168d5c17
--- /dev/null
+++ b/src/hooks/useFetchSpeakers.test.tsx
@@ -0,0 +1,257 @@
+import React, {FC} from "react";
+import {QueryClient, QueryClientProvider} from "react-query";
+import {renderHook, waitFor} from "@testing-library/react";
+import {useFetchSpeakers} from "./useFetchSpeakers";
+import axios, {AxiosHeaders, AxiosResponse} from "axios";
+import {speakerAdapter} from "../services/speakerAdapter";
+import {IResponse} from "../types/speakers";
+
+jest.mock("axios");
+const mockedAxios = axios as jest.Mocked;
+const axiosHeaders = new AxiosHeaders();
+
+const payload: AxiosResponse = {
+ status: 200,
+ statusText: "OK",
+ headers: {},
+ config: {
+ headers: axiosHeaders,
+ },
+ data: [
+ {
+ id: "1",
+ fullName: "John Smith",
+ profilePicture: "https://example.com/john.jpg",
+ tagLine: "Software engineer",
+ bio: "I am a software engineer",
+ sessions: [
+ {
+ id: 4567,
+ name: "sample session",
+ },
+ ],
+ links: [
+ {
+ linkType: "Twitter",
+ url: "https://twitter.com/johnsmith",
+ title: "",
+ },
+ {
+ linkType: "LinkedIn",
+ url: "https://linkedin.com/in/johnsmith",
+ title: "",
+ },
+ ],
+ },
+ {
+ id: "2",
+ fullName: "Jane Doe",
+ profilePicture: "https://example.com/jane.jpg",
+ tagLine: "Data scientist",
+ bio: "I am a data scientist",
+ sessions: [],
+ links: [
+ {
+ linkType: "Twitter",
+ url: "https://twitter.com/janedoe",
+ title: "",
+ },
+ {
+ linkType: "LinkedIn",
+ url: "https://linkedin.com/in/janedoe",
+ title: "",
+ },
+ ],
+ },
+ ],
+};
+
+describe("fetch speaker hook and speaker adapter", () => {
+ beforeAll(() => {
+ jest.mock("axios");
+ });
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it("should adapt from a server response with default URL", async () => {
+ const queryClient = new QueryClient();
+
+ mockedAxios.get.mockImplementation(() => Promise.resolve(payload));
+ const wrapper: FC>> = ({ children }) => {
+ return (
+
+ {children}
+
+ );
+ };
+
+ const { result } = renderHook(() => useFetchSpeakers(), {
+ wrapper,
+ });
+ await waitFor(() => result.current.isSuccess, {});
+ await waitFor(() => !result.current.isLoading, {});
+ expect(mockedAxios.get).toHaveBeenCalledWith("https://sessionize.com/api/v2/xhudniix/view/Speakers");
+ expect(result.current.isLoading).toEqual(false);
+ expect(result.current.error).toEqual(null);
+ expect(result.current.data).toEqual(speakerAdapter(payload.data));
+ });
+
+ it("should adapt from server response a query with id", async () => {
+ //Given
+ const queryClient = new QueryClient();
+ mockedAxios.get.mockResolvedValueOnce(payload);
+ const expectedPayload: IResponse[] = [
+ {
+ id: "1",
+ bio: "I am a software engineer",
+ fullName: "John Smith",
+ links: [
+ {
+ linkType: "LinkedIn",
+ url: "https://linkedin.com/in/johnsmith",
+ title: "",
+ },
+ {
+ url: "https://twitter.com/johnsmith",
+ title: "",
+ linkType: "Twitter",
+ },
+ ],
+ profilePicture: "https://example.com/john.jpg",
+ tagLine: "Software engineer",
+ sessions: [{ id: 4567, name: "sample session" }],
+ },
+ ];
+ const wrapper: FC>> = ({ children }) => {
+ return (
+
+ {children}
+
+ );
+ };
+
+ //When
+ const { result } = renderHook(() => useFetchSpeakers("1"), {
+ wrapper,
+ });
+ await waitFor(() => result.current.isSuccess);
+ await waitFor(() => !result.current.isLoading, {});
+ //then
+ expect(mockedAxios.get).toHaveBeenCalledWith("https://sessionize.com/api/v2/xhudniix/view/Speakers");
+ expect(result.current.data).toEqual(speakerAdapter(expectedPayload));
+ });
+
+ it("should use 2023 URL when '2023' is passed", async () => {
+ const queryClient = new QueryClient();
+ mockedAxios.get.mockImplementation(() => Promise.resolve(payload));
+ const wrapper: FC>> = ({ children }) => {
+ return (
+
+ {children}
+
+ );
+ };
+
+ const { result } = renderHook(() => useFetchSpeakers("2023"), {
+ wrapper,
+ });
+ await waitFor(() => result.current.isSuccess, {});
+ await waitFor(() => !result.current.isLoading, {});
+ expect(mockedAxios.get).toHaveBeenCalledWith("https://sessionize.com/api/v2/ttsitynd/view/Speakers");
+ expect(result.current.isLoading).toEqual(false);
+ expect(result.current.error).toEqual(null);
+ expect(result.current.data).toEqual(speakerAdapter(payload.data));
+ });
+
+ it("should use 2024 URL when '2024' is passed", async () => {
+ const queryClient = new QueryClient();
+ mockedAxios.get.mockImplementation(() => Promise.resolve(payload));
+ const wrapper: FC>> = ({ children }) => {
+ return (
+
+ {children}
+
+ );
+ };
+
+ const { result } = renderHook(() => useFetchSpeakers("2024"), {
+ wrapper,
+ });
+ await waitFor(() => result.current.isSuccess, {});
+ await waitFor(() => !result.current.isLoading, {});
+ expect(mockedAxios.get).toHaveBeenCalledWith("https://sessionize.com/api/v2/teq4asez/view/Speakers");
+ expect(result.current.isLoading).toEqual(false);
+ expect(result.current.error).toEqual(null);
+ expect(result.current.data).toEqual(speakerAdapter(payload.data));
+ });
+
+ it("should use custom URL when a URL is passed", async () => {
+ const queryClient = new QueryClient();
+ mockedAxios.get.mockImplementation(() => Promise.resolve(payload));
+ const wrapper: FC>> = ({ children }) => {
+ return (
+
+ {children}
+
+ );
+ };
+
+ const customUrl = "https://example.com/api/speakers";
+ const { result } = renderHook(() => useFetchSpeakers(customUrl), {
+ wrapper,
+ });
+ await waitFor(() => result.current.isSuccess, {});
+ await waitFor(() => !result.current.isLoading, {});
+ expect(mockedAxios.get).toHaveBeenCalledWith(customUrl);
+ expect(result.current.isLoading).toEqual(false);
+ expect(result.current.error).toEqual(null);
+ expect(result.current.data).toEqual(speakerAdapter(payload.data));
+ });
+
+ it("should filter by ID when both a URL and ID are passed", async () => {
+ //Given
+ const queryClient = new QueryClient();
+ mockedAxios.get.mockResolvedValueOnce(payload);
+ const expectedPayload: IResponse[] = [
+ {
+ id: "1",
+ bio: "I am a software engineer",
+ fullName: "John Smith",
+ links: [
+ {
+ linkType: "LinkedIn",
+ url: "https://linkedin.com/in/johnsmith",
+ title: "",
+ },
+ {
+ url: "https://twitter.com/johnsmith",
+ title: "",
+ linkType: "Twitter",
+ },
+ ],
+ profilePicture: "https://example.com/john.jpg",
+ tagLine: "Software engineer",
+ sessions: [{ id: 4567, name: "sample session" }],
+ },
+ ];
+ const wrapper: FC>> = ({ children }) => {
+ return (
+
+ {children}
+
+ );
+ };
+
+ //When
+ const customUrl = "https://example.com/api/speakers";
+ const { result } = renderHook(() => useFetchSpeakers(customUrl, "1"), {
+ wrapper,
+ });
+ await waitFor(() => result.current.isSuccess);
+ await waitFor(() => !result.current.isLoading, {});
+ //then
+ expect(mockedAxios.get).toHaveBeenCalledWith(customUrl);
+ expect(result.current.data).toEqual(speakerAdapter(expectedPayload));
+ });
+});
\ No newline at end of file
diff --git a/src/hooks/useFetchSpeakers.ts b/src/hooks/useFetchSpeakers.ts
new file mode 100644
index 000000000..74c649662
--- /dev/null
+++ b/src/hooks/useFetchSpeakers.ts
@@ -0,0 +1,45 @@
+import { useQuery, UseQueryResult } from "react-query";
+import axios from "axios";
+import { speakerAdapter } from "../services/speakerAdapter";
+import { ISpeaker } from "../types/speakers";
+
+const URLS = {
+ default: "https://sessionize.com/api/v2/xhudniix/view/Speakers",
+ 2023: "https://sessionize.com/api/v2/ttsitynd/view/Speakers",
+ 2024: "https://sessionize.com/api/v2/teq4asez/view/Speakers"
+};
+
+export const useFetchSpeakers = (urlOrId?: string, id?: string): UseQueryResult => {
+ // Determine if the first parameter is a URL or an ID
+ let url = URLS.default;
+ let speakerId = id;
+
+ if (urlOrId) {
+ // If urlOrId starts with http, it's a URL
+ if (urlOrId.startsWith("http")) {
+ url = urlOrId;
+ }
+ // If urlOrId is a year key in URLS, use that URL
+ else if (urlOrId in URLS) {
+ url = URLS[urlOrId as keyof typeof URLS];
+ }
+ // Otherwise, it's an ID
+ else {
+ speakerId = urlOrId;
+ }
+ }
+
+ return useQuery("api-speakers", async () => {
+ const serverResponse = await axios.get(url);
+ let returnData;
+ if (speakerId !== undefined) {
+ returnData = serverResponse.data.filter(
+ (speaker: { id: string }) => speaker.id === speakerId,
+ );
+ } else {
+ returnData = serverResponse.data;
+ }
+
+ return speakerAdapter(returnData);
+ });
+};
\ No newline at end of file
diff --git a/src/views/MeetingDetail/TalkDetailContainer2024.tsx b/src/views/MeetingDetail/TalkDetailContainer2024.tsx
index 38abcc52b..496a1e1dd 100644
--- a/src/views/MeetingDetail/TalkDetailContainer2024.tsx
+++ b/src/views/MeetingDetail/TalkDetailContainer2024.tsx
@@ -7,7 +7,7 @@ import {useParams} from "react-router";
import conferenceData from "../../data/2025.json";
import {useFetchTalksById} from "../Talks/UseFetchTalks";
import * as Sentry from "@sentry/react";
-import {useFetchSpeakers} from "../Speakers/UseFetchSpeakers";
+import {useFetchSpeakers} from "../../hooks/useFetchSpeakers";
import MeetingDetail from "./MeetingDetail";
import {ISpeaker} from "../../types/speakers";
import {sessionAdapter} from "../../services/sessionsAdapter";
diff --git a/src/views/SpeakerDetail/SpeakerDetailContainer.tsx b/src/views/SpeakerDetail/SpeakerDetailContainer.tsx
index 4c4da7486..9c0a9ea4a 100644
--- a/src/views/SpeakerDetail/SpeakerDetailContainer.tsx
+++ b/src/views/SpeakerDetail/SpeakerDetailContainer.tsx
@@ -6,7 +6,7 @@ import SpeakerDetail from "./SpeakerDetail";
import { useParams } from "react-router";
import { StyledContainer, StyledWaveContainer } from "./Speaker.style";
import conferenceData from "../../data/2025.json";
-import { useFetchSpeakers } from "../Speakers/UseFetchSpeakers";
+import { useFetchSpeakers } from "../../hooks/useFetchSpeakers";
import * as Sentry from "@sentry/react";
const SpeakerDetailContainer: FC> = () => {
diff --git a/src/views/Speakers/Speakers.tsx b/src/views/Speakers/Speakers.tsx
index f2525ffff..ee918abe3 100644
--- a/src/views/Speakers/Speakers.tsx
+++ b/src/views/Speakers/Speakers.tsx
@@ -20,7 +20,7 @@ import {
import webData from "../../data/2024.json";
import Button from "../../components/UI/Button";
import {gaEventTracker} from "../../components/analytics/Analytics";
-import {useFetchSpeakers} from "./UseFetchSpeakers";
+import {useFetchSpeakers} from "../../hooks/useFetchSpeakers";
import * as Sentry from "@sentry/react";
import {ISpeaker} from "../../types/speakers";
diff --git a/src/views/Speakers/UseFetchSpeakers.test.tsx b/src/views/Speakers/UseFetchSpeakers.test.tsx
deleted file mode 100644
index 7e8e1ed78..000000000
--- a/src/views/Speakers/UseFetchSpeakers.test.tsx
+++ /dev/null
@@ -1,144 +0,0 @@
-import React, {FC} from "react";
-import {QueryClient, QueryClientProvider} from "react-query";
-import {renderHook, waitFor} from "@testing-library/react";
-import {useFetchSpeakers} from "./UseFetchSpeakers";
-import axios, {AxiosHeaders, AxiosResponse} from "axios";
-import {speakerAdapter} from "../../services/speakerAdapter";
-import {IResponse} from "../../types/speakers";
-
-jest.mock("axios");
-const mockedAxios = axios as jest.Mocked;
-const axiosHeaders = new AxiosHeaders();
-
-const payload: AxiosResponse = {
- status: 200,
- statusText: "OK",
- headers: {},
- config: {
- headers: axiosHeaders,
- },
- data: [
- {
- id: "1",
- fullName: "John Smith",
- profilePicture: "https://example.com/john.jpg",
- tagLine: "Software engineer",
- bio: "I am a software engineer",
- sessions: [
- {
- id: 4567,
- name: "sample session",
- },
- ],
- links: [
- {
- linkType: "Twitter",
- url: "https://twitter.com/johnsmith",
- title: "",
- },
- {
- linkType: "LinkedIn",
- url: "https://linkedin.com/in/johnsmith",
- title: "",
- },
- ],
- },
- {
- id: "2",
- fullName: "Jane Doe",
- profilePicture: "https://example.com/jane.jpg",
- tagLine: "Data scientist",
- bio: "I am a data scientist",
- sessions: [],
- links: [
- {
- linkType: "Twitter",
- url: "https://twitter.com/janedoe",
- title: "",
- },
- {
- linkType: "LinkedIn",
- url: "https://linkedin.com/in/janedoe",
- title: "",
- },
- ],
- },
- ],
-};
-
-describe("fetch speaker hook and speaker adapter", () => {
- beforeAll(() => {
- jest.mock("axios");
- });
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
- it.skip("should adapt from a server response", async () => {
- const queryClient = new QueryClient();
-
- mockedAxios.get.mockImplementation(() => Promise.resolve(payload));
- const wrapper: FC>> = ({ children }) => {
- return (
-
- {children}
-
- );
- };
-
- const { result } = renderHook(() => useFetchSpeakers(), {
- wrapper,
- });
- await waitFor(() => result.current.isSuccess, {});
- await waitFor(() => !result.current.isLoading, {});
- expect(mockedAxios.get).toHaveBeenCalled();
- expect(result.current.isLoading).toEqual(false);
- expect(result.current.error).toEqual(null);
- expect(result.current.data).toEqual(speakerAdapter(payload.data));
- });
-
- it.skip("should adapt from server response a query with id", async () => {
- //Given
- const queryClient = new QueryClient();
- mockedAxios.get.mockResolvedValueOnce(payload);
- const expectedPayload: IResponse[] = [
- {
- id: "1",
- bio: "I am a software engineer",
- fullName: "John Smith",
- links: [
- {
- linkType: "LinkedIn",
- url: "https://linkedin.com/in/johnsmith",
- title: "",
- },
- {
- url: "https://twitter.com/johnsmith",
- title: "",
- linkType: "Twitter",
- },
- ],
- profilePicture: "https://example.com/john.jpg",
- tagLine: "Software engineer",
- sessions: [{ id: 4567, name: "sample session" }],
- },
- ];
- const wrapper: FC>> = ({ children }) => {
- return (
-
- {children}
-
- );
- };
-
- //When
- const { result } = renderHook(() => useFetchSpeakers("1"), {
- wrapper,
- });
- await waitFor(() => result.current.isSuccess);
- await waitFor(() => !result.current.isLoading, {});
- //then
- expect(mockedAxios.get).toHaveBeenCalled();
- expect(result.current.data).toEqual(speakerAdapter(expectedPayload));
- });
-});
diff --git a/src/views/Speakers/UseFetchSpeakers.ts b/src/views/Speakers/UseFetchSpeakers.ts
deleted file mode 100644
index 8ff0fecb2..000000000
--- a/src/views/Speakers/UseFetchSpeakers.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { useQuery, UseQueryResult } from "react-query";
-import axios from "axios";
-import { speakerAdapter } from "../../services/speakerAdapter";
-import { ISpeaker } from "../../types/speakers";
-
-export const useFetchSpeakers = (id?: string): UseQueryResult => {
- return useQuery("api-speakers", async () => {
- const serverResponse = await axios.get(
- "https://sessionize.com/api/v2/xhudniix/view/Speakers",
- );
- let returnData;
- if (id !== undefined) {
- returnData = serverResponse.data.filter(
- (speaker: { id: string }) => speaker.id === id,
- );
- } else {
- returnData = serverResponse.data;
- }
-
- return speakerAdapter(returnData);
- });
-};
From f697ecdec3bb77b12672236480a3d4d2df5aec3d Mon Sep 17 00:00:00 2001
From: Anyul Rivas
Date: Tue, 25 Mar 2025 00:04:34 +0100
Subject: [PATCH 02/11] Update src/hooks/useFetchSpeakers.ts
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
---
src/hooks/useFetchSpeakers.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/hooks/useFetchSpeakers.ts b/src/hooks/useFetchSpeakers.ts
index 74c649662..94e0b2fe5 100644
--- a/src/hooks/useFetchSpeakers.ts
+++ b/src/hooks/useFetchSpeakers.ts
@@ -9,7 +9,7 @@ const URLS = {
2024: "https://sessionize.com/api/v2/teq4asez/view/Speakers"
};
-export const useFetchSpeakers = (urlOrId?: string, id?: string): UseQueryResult => {
+export const useFetchSpeakers = (yearOrUrl?: string, id?: string): UseQueryResult => {
// Determine if the first parameter is a URL or an ID
let url = URLS.default;
let speakerId = id;
From 9fcb831ee56f189e67bcf8d10280e80cbc9aae73 Mon Sep 17 00:00:00 2001
From: Anyul Rivas
Date: Tue, 25 Mar 2025 08:49:52 +0100
Subject: [PATCH 03/11] refactor: remove duplication on fetch Talks logic
---
.../TalkDetail/TalkDetailContainer2023.tsx | 18 +-
src/2023/Talks/Talks2023.tsx | 6 +-
src/2023/Talks/UseFetchTalks.ts | 26 --
src/2023/Workshops/Workshops2023.tsx | 36 +-
.../TalkDetail/MeetingDetailContainer.tsx | 102 ++---
src/2024/Talks/LiveView.tsx | 100 ++---
src/2024/Talks/Talks2024.tsx | 226 +++++-----
src/2024/Talks/UseFetchTalks.ts | 32 --
src/2024/Talks/useFetchTalks.test.tsx | 423 ------------------
src/hooks/useFetchTalks.test.tsx | 371 +++++++++++++++
src/hooks/useFetchTalks.ts | 77 ++++
.../MeetingDetail/TalkDetailContainer2024.tsx | 16 +-
src/views/Talks/LiveView.tsx | 2 +-
src/views/Talks/Talks.tsx | 2 +-
src/views/Talks/UseFetchTalks.ts | 32 --
src/views/Talks/useFetchTalks.test.tsx | 422 -----------------
src/views/Workshops/Workshops.tsx | 22 +-
17 files changed, 710 insertions(+), 1203 deletions(-)
delete mode 100644 src/2023/Talks/UseFetchTalks.ts
delete mode 100644 src/2024/Talks/UseFetchTalks.ts
delete mode 100644 src/2024/Talks/useFetchTalks.test.tsx
create mode 100644 src/hooks/useFetchTalks.test.tsx
create mode 100644 src/hooks/useFetchTalks.ts
delete mode 100644 src/views/Talks/UseFetchTalks.ts
delete mode 100644 src/views/Talks/useFetchTalks.test.tsx
diff --git a/src/2023/TalkDetail/TalkDetailContainer2023.tsx b/src/2023/TalkDetail/TalkDetailContainer2023.tsx
index 80b6fd597..23a6f2259 100644
--- a/src/2023/TalkDetail/TalkDetailContainer2023.tsx
+++ b/src/2023/TalkDetail/TalkDetailContainer2023.tsx
@@ -1,24 +1,24 @@
-import {Color} from "../../styles/colors";
-import React, {FC, useEffect} from "react";
+import { Color } from "../../styles/colors";
+import React, { FC, useEffect } from "react";
import NotFoundError from "../../components/NotFoundError/NotFoundError";
import SectionWrapper from "../../components/SectionWrapper/SectionWrapper";
import styled from "styled-components";
-import {useParams} from "react-router";
+import { useParams } from "react-router";
import conferenceData from "../../data/2023.json";
-import {useFetchTalksById} from "../Talks/UseFetchTalks";
+import { useFetchTalksById } from "../../hooks/useFetchTalks";
import * as Sentry from "@sentry/react";
-import {useFetchSpeakers} from "../../hooks/useFetchSpeakers";
-import {Session} from "../../types/sessions";
+import { useFetchSpeakers } from "../../hooks/useFetchSpeakers";
+import { Session } from "../../types/sessions";
import TalkDetail from "./TalkDetail";
-import {ISpeaker} from "../../types/speakers";
-import {sessionAdapter} from "../../services/sessionsAdapter";
+import { ISpeaker } from "../../types/speakers";
+import { sessionAdapter } from "../../services/sessionsAdapter";
const StyledContainer = styled.div`
background-color: ${Color.WHITE};
`;
const TalkDetailContainer2023: FC> = () => {
const { id } = useParams<{ id: string }>();
- const { isLoading, error, data } = useFetchTalksById(id!);
+ const { isLoading, error, data } = useFetchTalksById(id!, "2023");
const { data: speakerData } = useFetchSpeakers("2023");
const getTalkSpeakers = (
diff --git a/src/2023/Talks/Talks2023.tsx b/src/2023/Talks/Talks2023.tsx
index b7759c444..90468590f 100644
--- a/src/2023/Talks/Talks2023.tsx
+++ b/src/2023/Talks/Talks2023.tsx
@@ -12,7 +12,7 @@ import {
StyledTitleIcon,
StyledWaveContainer,
} from "./Talks.style";
-import { useFetchTalks } from "./UseFetchTalks";
+import { useFetchTalks } from "../../hooks/useFetchTalks";
import * as Sentry from "@sentry/react";
import { Dropdown, DropdownChangeEvent } from "primereact/dropdown";
import "primereact/resources/primereact.min.css";
@@ -27,9 +27,9 @@ interface TrackInfo {
const Talks2023: FC> = () => {
const [selectedGroupId, setSelectedGroupId] = useState(
- null
+ null,
);
- const { isLoading, error, data } = useFetchTalks();
+ const { isLoading, error, data } = useFetchTalks("2023");
useEffect(() => {
const sessionSelectedGroupCode =
diff --git a/src/2023/Talks/UseFetchTalks.ts b/src/2023/Talks/UseFetchTalks.ts
deleted file mode 100644
index dd5ee6fe0..000000000
--- a/src/2023/Talks/UseFetchTalks.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import {useQuery, UseQueryResult} from "react-query";
-
-import axios from "axios";
-import {IGroup, Session} from "../../types/sessions";
-
-export const useFetchTalks = (): UseQueryResult =>
- useQuery("api-talks", async () => {
- let data = await axios.get(
- "https://sessionize.com/api/v2/ttsitynd/view/Sessions"
- );
- return data.data;
- });
-
-export const useFetchTalksById = (id: string): UseQueryResult =>
- useQuery("talks", async () => {
- const serverResponse = await axios.get(
- "https://sessionize.com/api/v2/ttsitynd/view/Sessions"
- );
- return serverResponse.data
- .map((track: IGroup) => track.sessions)
- .flat(1)
- .filter((session: { id: string }) => session.id === id);
- });
-
-
-
diff --git a/src/2023/Workshops/Workshops2023.tsx b/src/2023/Workshops/Workshops2023.tsx
index f80ac7d3d..78b6c90d0 100644
--- a/src/2023/Workshops/Workshops2023.tsx
+++ b/src/2023/Workshops/Workshops2023.tsx
@@ -11,32 +11,32 @@ import {
import LessThanDarkBlueIcon from "../../assets/images/LessThanDarkBlueIcon.svg";
import TitleSection from "../../components/SectionTitle/TitleSection";
import MoreThanBlueIcon from "../../assets/images/MoreThanBlueIcon.svg";
-import { useFetchTalks } from "../Talks/UseFetchTalks";
+import { useFetchTalks } from "../../hooks/useFetchTalks";
import * as Sentry from "@sentry/react";
import conferenceData from "../../data/2023.json";
import styled from "styled-components";
import { BIG_BREAKPOINT } from "../../constants/BreakPoints";
-import {TalkCard} from "../../components/Talk/TalkCard";
+import { TalkCard } from "../../components/Talk/TalkCard";
const StyledSection = styled.section`
- {
+{
display: flex;
padding: 0 10rem;
flex-wrap: wrap;
- }
+}
- @media (max-width: ${BIG_BREAKPOINT}px) {
- padding: 1rem;
- flex-direction: column;
- }
+ @media (max-width: ${BIG_BREAKPOINT}px) {
+ padding: 1rem;
+ flex-direction: column;
+ }
- & > div {
- margin: 1rem;
- min-width: 14%;
- }
+ & > div {
+ margin: 1rem;
+ min-width: 14%;
+ }
`;
const Workshops2023: FC> = () => {
- const { isLoading, data, error } = useFetchTalks();
+ const { isLoading, data, error } = useFetchTalks("2023");
useEffect(() => {
document.title = `Workshops - DevBcn - ${conferenceData.edition}`;
}, []);
@@ -49,8 +49,8 @@ const Workshops2023: FC> = () => {
?.flatMap((group) => group.sessions)
.filter((session) =>
session.categories.some((category) =>
- category.categoryItems.some((item) => categoryItemIds.has(item.id))
- )
+ category.categoryItems.some((item) => categoryItemIds.has(item.id)),
+ ),
);
//endregion
@@ -98,11 +98,7 @@ const Workshops2023: FC> = () => {
)}
{workshops?.map((track) => (
-
+
))}
diff --git a/src/2024/TalkDetail/MeetingDetailContainer.tsx b/src/2024/TalkDetail/MeetingDetailContainer.tsx
index ef30c0d69..3c598d16b 100644
--- a/src/2024/TalkDetail/MeetingDetailContainer.tsx
+++ b/src/2024/TalkDetail/MeetingDetailContainer.tsx
@@ -1,71 +1,71 @@
-import {Color} from "../../styles/colors";
-import React, {FC, useEffect} from "react";
+import { Color } from "../../styles/colors";
+import React, { FC, useEffect } from "react";
import NotFoundError from "../../components/NotFoundError/NotFoundError";
import SectionWrapper from "../../components/SectionWrapper/SectionWrapper";
import styled from "styled-components";
-import {useParams} from "react-router";
+import { useParams } from "react-router";
import conferenceData from "../../data/2024.json";
-import {useFetchTalksById} from "../Talks/UseFetchTalks";
+import { useFetchTalksById } from "../../hooks/useFetchTalks";
import * as Sentry from "@sentry/react";
-import {useFetchSpeakers} from "../../hooks/useFetchSpeakers";
+import { useFetchSpeakers } from "../../hooks/useFetchSpeakers";
import MeetingDetail from "./MeetingDetail";
-import {ISpeaker} from "../../types/speakers";
-import {Session} from "../../types/sessions";
-import {sessionAdapter} from "../../services/sessionsAdapter";
+import { ISpeaker } from "../../types/speakers";
+import { Session } from "../../types/sessions";
+import { sessionAdapter } from "../../services/sessionsAdapter";
const StyledContainer = styled.div`
- background-color: ${Color.WHITE};
+ background-color: ${Color.WHITE};
`;
const MeetingDetailContainer: FC> = () => {
- const {id} = useParams<{ id: string }>();
- const {isLoading, error, data} = useFetchTalksById(id!);
- const {data: speakerData} = useFetchSpeakers("2024");
+ const { id } = useParams<{ id: string }>();
+ const { isLoading, error, data } = useFetchTalksById(id!, "2024");
+ const { data: speakerData } = useFetchSpeakers("2024");
- const getTalkSpeakers = (
- data: Session[] | undefined,
- ): string[] | undefined => {
- const speakers = data?.[0]?.speakers;
- return speakers?.map((speaker) => speaker.id);
- };
+ const getTalkSpeakers = (
+ data: Session[] | undefined,
+ ): string[] | undefined => {
+ const speakers = data?.[0]?.speakers;
+ return speakers?.map((speaker) => speaker.id);
+ };
- const talkSpeakers: string[] | undefined = getTalkSpeakers(data);
- const sessionSpeakers: ISpeaker[] | undefined = speakerData?.filter(
- (speaker) => talkSpeakers?.includes(speaker.id),
- );
+ const talkSpeakers: string[] | undefined = getTalkSpeakers(data);
+ const sessionSpeakers: ISpeaker[] | undefined = speakerData?.filter(
+ (speaker) => talkSpeakers?.includes(speaker.id),
+ );
- const adaptedMeeting = sessionAdapter(data?.at(0));
+ const adaptedMeeting = sessionAdapter(data?.at(0));
- useEffect(() => {
- document.title = `${data?.at(0)?.title} - DevBcn - ${
- conferenceData.edition
- }`;
- }, [data]);
+ useEffect(() => {
+ document.title = `${data?.at(0)?.title} - DevBcn - ${
+ conferenceData.edition
+ }`;
+ }, [data]);
- if (error) {
- Sentry.captureException(error);
- }
+ if (error) {
+ Sentry.captureException(error);
+ }
- return (
-
-
- {isLoading && Loading
}
- {!isLoading &&
- sessionSpeakers !== undefined &&
- sessionSpeakers.length > 0 &&
- adaptedMeeting !== undefined && (
-
- )}
- {!isLoading &&
- (!sessionSpeakers ||
- sessionSpeakers.length === 0 ||
- !adaptedMeeting) && }
-
-
- );
+ return (
+
+
+ {isLoading && Loading
}
+ {!isLoading &&
+ sessionSpeakers !== undefined &&
+ sessionSpeakers.length > 0 &&
+ adaptedMeeting !== undefined && (
+
+ )}
+ {!isLoading &&
+ (!sessionSpeakers ||
+ sessionSpeakers.length === 0 ||
+ !adaptedMeeting) && }
+
+
+ );
};
export default MeetingDetailContainer;
diff --git a/src/2024/Talks/LiveView.tsx b/src/2024/Talks/LiveView.tsx
index e0240b0e4..5a64ff975 100644
--- a/src/2024/Talks/LiveView.tsx
+++ b/src/2024/Talks/LiveView.tsx
@@ -1,65 +1,65 @@
-import React, {FC, useCallback, useEffect, useMemo} from "react";
-import {useFetchLiveView} from "./UseFetchTalks";
+import React, { FC, useCallback, useEffect, useMemo } from "react";
+import { useFetchLiveView } from "../../hooks/useFetchTalks";
import Loading from "../../components/Loading/Loading";
import conference from "../../data/2024.json";
import * as Sentry from "@sentry/react";
-import {UngroupedSession} from "../../views/Talks/liveView.types";
-import {TalkCard} from "../../views/Talks/components/TalkCard";
-import {talkCardAdapter} from "../../views/Talks/TalkCardAdapter";
-import {StyledMain} from "../../views/Talks/Talks.style";
+import { UngroupedSession } from "../../views/Talks/liveView.types";
+import { TalkCard } from "../../views/Talks/components/TalkCard";
+import { talkCardAdapter } from "../../views/Talks/TalkCardAdapter";
+import { StyledMain } from "../../views/Talks/Talks.style";
const LiveView: FC> = () => {
- const {isLoading, error, data} = useFetchLiveView();
- const today = useMemo(() => new Date(), []);
+ const { isLoading, error, data } = useFetchLiveView("2024");
+ const today = useMemo(() => new Date(), []);
- const isBetween = useCallback(
- (today: Date, startDate: string, endDate: string): boolean => {
- return today >= new Date(startDate) && today <= new Date(endDate);
- },
- [],
- );
+ const isBetween = useCallback(
+ (today: Date, startDate: string, endDate: string): boolean => {
+ return today >= new Date(startDate) && today <= new Date(endDate);
+ },
+ [],
+ );
- const getPredicate = useCallback(
- () => (session: UngroupedSession) =>
- isBetween(today, session.startsAt, session.endsAt),
- [today, isBetween],
- );
+ const getPredicate = useCallback(
+ () => (session: UngroupedSession) =>
+ isBetween(today, session.startsAt, session.endsAt),
+ [today, isBetween],
+ );
- const filteredTalks = useMemo(() => {
- return data?.sessions?.filter(getPredicate());
- }, [data, getPredicate]);
+ const filteredTalks = useMemo(() => {
+ return data?.sessions?.filter(getPredicate());
+ }, [data, getPredicate]);
- useEffect(() => {
- document.title = `Live view - ${conference.title} - ${conference.edition} Edition`;
- }, []);
+ useEffect(() => {
+ document.title = `Live view - ${conference.title} - ${conference.edition} Edition`;
+ }, []);
- useEffect(() => {
- if (error) {
- Sentry.captureException(error);
- }
- }, [error]);
+ useEffect(() => {
+ if (error) {
+ Sentry.captureException(error);
+ }
+ }, [error]);
- return (
-
-
-
- {conference.title} - {conference.edition} Edition
-
+ return (
+
+
+
+ {conference.title} - {conference.edition} Edition
+
- {isLoading && }
- Live Schedule
- {!isBetween(today, conference.startDay, conference.endDay) && (
- The live schedule is not ready yet
- )}
- {filteredTalks?.map((session) => (
-
- ))}
-
- );
+ {isLoading && }
+ Live Schedule
+ {!isBetween(today, conference.startDay, conference.endDay) && (
+ The live schedule is not ready yet
+ )}
+ {filteredTalks?.map((session) => (
+
+ ))}
+
+ );
};
export default LiveView;
diff --git a/src/2024/Talks/Talks2024.tsx b/src/2024/Talks/Talks2024.tsx
index c860a5909..6375fa541 100644
--- a/src/2024/Talks/Talks2024.tsx
+++ b/src/2024/Talks/Talks2024.tsx
@@ -1,145 +1,143 @@
-import React, {FC, useEffect, useState} from "react";
+import React, { FC, useEffect, useState } from "react";
import LessThanDarkBlueIcon from "../../assets/images/LessThanDarkBlueIcon.svg";
import MoreThanBlueIcon from "../../assets/images/MoreThanBlueIcon.svg";
import SectionWrapper from "../../components/SectionWrapper/SectionWrapper";
import TitleSection from "../../components/SectionTitle/TitleSection";
-import {Color} from "../../styles/colors";
+import { Color } from "../../styles/colors";
import conferenceData from "../../data/2024.json";
-import {useFetchTalks} from "./UseFetchTalks";
+import { useFetchTalks } from "../../hooks/useFetchTalks";
import * as Sentry from "@sentry/react";
-import {Dropdown, DropdownChangeEvent} from "primereact/dropdown";
+import { Dropdown, DropdownChangeEvent } from "primereact/dropdown";
import "primereact/resources/primereact.min.css";
import "primereact/resources/themes/lara-light-indigo/theme.css";
import "../../styles/theme.css";
import {
- StyledMarginBottom,
- StyledSpeakersSection,
- StyledTitleContainer,
- StyledTitleIcon,
- StyledWaveContainer
+ StyledMarginBottom,
+ StyledSpeakersSection,
+ StyledTitleContainer,
+ StyledTitleIcon,
+ StyledWaveContainer,
} from "../../views/Talks/Talks.style";
import TrackInformation from "../../components/Talk/TrackInformation";
interface TrackInfo {
- name: string;
- code?: string;
+ name: string;
+ code?: string;
}
const Talks2024: FC> = () => {
- const [selectedGroupId, setSelectedGroupId] = useState(
- null,
- );
- const {isLoading, error, data} = useFetchTalks();
+ const [selectedGroupId, setSelectedGroupId] = useState(
+ null,
+ );
+ const { isLoading, error, data } = useFetchTalks("2024");
- useEffect(() => {
- const sessionSelectedGroupCode =
- sessionStorage.getItem("selectedGroupCode");
- const sessionSelectedGroupName =
- sessionStorage.getItem("selectedGroupName");
+ useEffect(() => {
+ const sessionSelectedGroupCode =
+ sessionStorage.getItem("selectedGroupCode");
+ const sessionSelectedGroupName =
+ sessionStorage.getItem("selectedGroupName");
- document.title = `Talks - ${conferenceData.title} - ${conferenceData.edition}`;
+ document.title = `Talks - ${conferenceData.title} - ${conferenceData.edition}`;
- if (sessionSelectedGroupCode && sessionSelectedGroupName) {
- setSelectedGroupId({
- name: sessionSelectedGroupName,
- code: sessionSelectedGroupCode,
- });
- }
- }, []);
-
- if (error) {
- Sentry.captureException(error);
+ if (sessionSelectedGroupCode && sessionSelectedGroupName) {
+ setSelectedGroupId({
+ name: sessionSelectedGroupName,
+ code: sessionSelectedGroupCode,
+ });
}
+ }, []);
+
+ if (error) {
+ Sentry.captureException(error);
+ }
- const dropDownOptions = [
- {name: "All Tracks", code: undefined},
- ...(data !== undefined
- ? data.flatMap((group) => ({
- code: group.groupId.toString(),
- name: group.groupName,
- }))
- : []),
- ];
+ const dropDownOptions = [
+ { name: "All Tracks", code: undefined },
+ ...(data !== undefined
+ ? data.flatMap((group) => ({
+ code: group.groupId.toString(),
+ name: group.groupName,
+ }))
+ : []),
+ ];
- const filteredTalks = selectedGroupId?.code
- ? data?.filter((talk) => talk.groupId.toString() === selectedGroupId.code)
- : data;
+ const filteredTalks = selectedGroupId?.code
+ ? data?.filter((talk) => talk.groupId.toString() === selectedGroupId.code)
+ : data;
- const onChangeSelectedTrack = (e: DropdownChangeEvent) => {
- const value = e.value;
- setSelectedGroupId(value || null);
- sessionStorage.setItem("selectedGroupCode", value?.code || "");
- sessionStorage.setItem("selectedGroupName", value?.name || "");
- };
- return (
- <>
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
- {isLoading && Loading
}
- {conferenceData.hideTalks ? (
-
- No talks selected yet. Keep in touch in our social
- media for
- upcoming announcements
-
- ) : (
- filteredTalks &&
- Array.isArray(filteredTalks) && (
- <>
-
-
-
-
- {filteredTalks.map((track) => (
-
- ))}
- >
- )
- )}
-
-
-
- >
- );
+ color={Color.WHITE}
+ />
+
+
+
+
+
+
+
+
+
+ {isLoading && Loading
}
+ {conferenceData.hideTalks ? (
+
+ No talks selected yet. Keep in touch in our social media for
+ upcoming announcements
+
+ ) : (
+ filteredTalks &&
+ Array.isArray(filteredTalks) && (
+ <>
+
+
+
+
+ {filteredTalks.map((track) => (
+
+ ))}
+ >
+ )
+ )}
+
+
+
+ >
+ );
};
export default Talks2024;
diff --git a/src/2024/Talks/UseFetchTalks.ts b/src/2024/Talks/UseFetchTalks.ts
deleted file mode 100644
index 9b08dbf4e..000000000
--- a/src/2024/Talks/UseFetchTalks.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import {useQuery, UseQueryResult} from "react-query";
-import axios from "axios";
-import {Liveview} from "../../views/Talks/liveView.types";
-import {IGroup, Session} from "../../types/sessions";
-
-export const useFetchTalks = (): UseQueryResult =>
- useQuery("api-talks", async () => {
- let data = await axios.get(
- "https://sessionize.com/api/v2/teq4asez/view/Sessions",
- );
- return data.data;
- });
-
-export const useFetchTalksById = (id: string): UseQueryResult =>
- useQuery("talks", async () => {
- const serverResponse = await axios.get(
- "https://sessionize.com/api/v2/teq4asez/view/Sessions",
- );
- return serverResponse.data
- .map((track: IGroup) => track.sessions)
- .flat(1)
- .filter((session: { id: string }) => session.id === id);
- });
-
-export const useFetchLiveView = (): UseQueryResult =>
- useQuery("api-talks", async () => {
- let data = await axios.get(
- "https://sessionize.com/api/v2/ezm48alx/view/Sessions",
- );
- return data.data.at(0);
- });
-
diff --git a/src/2024/Talks/useFetchTalks.test.tsx b/src/2024/Talks/useFetchTalks.test.tsx
deleted file mode 100644
index 24d426a26..000000000
--- a/src/2024/Talks/useFetchTalks.test.tsx
+++ /dev/null
@@ -1,423 +0,0 @@
-import React, {FC} from "react";
-import {QueryClient, QueryClientProvider} from "react-query";
-import {renderHook, waitFor} from "@testing-library/react";
-import axios, {AxiosHeaders, AxiosResponse} from "axios";
-import {faker} from "@faker-js/faker";
-import {useFetchLiveView, useFetchTalksById,} from "./UseFetchTalks";
-import {UngroupedSession} from "../../views/Talks/liveView.types";
-import {
- CategoryItemEnum,
- IMeeting,
- QuestionAnswers,
- Session,
- SessionCategory
-} from "../../types/sessions";
-import {
- extractSessionCategoryInfo,
- extractSessionSlides,
- extractSessionTags,
- sessionAdapter
-} from "../../services/sessionsAdapter";
-
-
-jest.mock("axios");
-const mockedAxios = axios as jest.Mocked;
-const axiosHeaders = new AxiosHeaders();
-const queryClient = new QueryClient();
-const wrapper: FC>> = ({
- children,
- }) => (
- {children}
-);
-
-describe("sessionAdapter", () => {
- test("returns empty strings when session is undefined", () => {
- expect(sessionAdapter(undefined)).toBeUndefined();
- });
-
- test("returns the expected output when session is defined", () => {
- const session: Session = {
- track: "Java ( core frameworks & libraries )",
- id: 5000,
- description: "Session description",
- startsAt: "2024-06-13T12:00:00",
- endsAt: "2024-06-13T14:00:00",
- title: "Session title",
- speakers: [
- {
- id: "6f672350-1c71-4a6e-a382-2b1db6e631fd",
- name: "Eric Deandrea",
- },
- {
- id: "4452d53b-603f-4185-beab-766a19258c0f",
- name: "Holly Cummins",
- },
- ],
- recordingUrl: "https://example.com/video.mp4",
- questionAnswers: [
- {
- id: 47395,
- question: "Tags/Topics",
- questionType: "Short_Text",
- answer: "java,openjdk",
- },
- {
- id: 3425,
- question: "Slides",
- questionType: "web_address",
- answer: "https://www.google.com",
- },
- ],
- categories: [
- {
- id: 45078,
- name: CategoryItemEnum.Format,
- categoryItems: [
- {
- id: 149212,
- name: "Session",
- },
- ],
- },
- {
- id: 45079,
- name: CategoryItemEnum.Track,
- categoryItems: [
- {
- id: 159116,
- name: "Java ( core frameworks & libraries )",
- },
- ],
- },
- {
- id: 45080,
- name: CategoryItemEnum.Level,
- categoryItems: [
- {
- id: 149217,
- name: "Introductory and overview",
- },
- ],
- },
- {
- id: 45081,
- name: CategoryItemEnum.Language,
- categoryItems: [
- {
- id: 149221,
- name: "English",
- },
- ],
- },
- ],
- };
- const expected: IMeeting = {
- id: 5000,
- description: "Session description",
- title: "Session title",
- speakers: [
- {
- id: "6f672350-1c71-4a6e-a382-2b1db6e631fd",
- name: "Eric Deandrea",
- },
- {
- id: "4452d53b-603f-4185-beab-766a19258c0f",
- name: "Holly Cummins",
- },
- ],
- videoUrl: "https://example.com/video.mp4",
- slidesURL: "https://www.google.com",
- videoTags: ["java", "openjdk"],
- level: "Introductory and overview ⭐",
- language: "English 🇬🇧",
- type: "Session 🗣",
- track: "Java ( core frameworks & libraries )",
- startDate: "2024-06-13",
- startTime: "12:00:00",
- endDate: "2024-06-13",
- endTime: "14:00:00",
- };
-
- expect(sessionAdapter(session)).toEqual(expected);
- });
-});
-
-describe("extractSessionTags", () => {
- test("returns undefined when questionAnswers is empty", () => {
- expect(extractSessionTags([])).toBeUndefined();
- });
-
- test("returns undefined when questionAnswers do not have a Tags/Topics question", () => {
- const questionAnswers: QuestionAnswers[] = [
- {
- id: 45775,
- question: "Question 1",
- answer: "Answer 1",
- questionType: "Short_Text",
- },
- {
- id: 999,
- question: "Question 2",
- answer: "Answer 2",
- questionType: "Short_Text",
- },
- ];
-
- expect(extractSessionTags(questionAnswers)).toBeUndefined();
- });
-
- test("returns the expected output when questionAnswers have a Tags/Topics question", () => {
- const questionAnswers: QuestionAnswers[] = [
- {
- id: 1,
- question: "Question 1",
- answer: "Answer 1",
- questionType: "Short_Text",
- },
- {
- id: 2,
- question: "Tags/Topics",
- answer: "tag1, tag2, tag3",
- questionType: "Short_Text",
- },
- {
- id: 3,
- question: "Question 2",
- answer: "Answer 2",
- questionType: "Short_Text",
- },
- ];
-
- expect(extractSessionTags(questionAnswers)).toEqual([
- "tag1",
- " tag2",
- " tag3",
- ]);
- });
-});
-
-describe("extractSessionSlides", () => {
- test("returns empty when questionAnswers is empty", () => {
- expect(extractSessionSlides([])).toEqual("");
- });
-
- test("returns the expected output when questionAnswers have a Slides question", () => {
- const questionAnswers: QuestionAnswers[] = [
- {
- id: 1,
- question: "Question 1",
- answer: "Answer 1",
- questionType: "Short_Text",
- },
- {
- id: 2,
- question: "Slides",
- answer: "https://www.google.com",
- questionType: "Short_Text",
- },
- {
- id: 3,
- question: "Question 2",
- answer: "Answer 2",
- questionType: "Short_Text",
- },
- ];
-
- expect(extractSessionSlides(questionAnswers)).toEqual(
- "https://www.google.com",
- );
- });
-});
-
-describe("extractSessionCategoryInfo", () => {
- const categories: SessionCategory[] = [
- {
- id: 4,
- name: CategoryItemEnum.Level,
- categoryItems: [
- {id: 1, name: "Introductory and overview"},
- {id: 2, name: "Intermediate"},
- ],
- },
- {
- id: 8,
- name: CategoryItemEnum.Language,
- categoryItems: [
- {id: 3, name: "English"},
- {id: 4, name: "Spanish"},
- ],
- },
- ];
-
- test("returns undefined when categories is empty", () => {
- expect(
- extractSessionCategoryInfo([], CategoryItemEnum.Level),
- ).toBeUndefined();
- });
-
- test("returns undefined when the requested item is not present in categories", () => {
- expect(
- extractSessionCategoryInfo(categories, CategoryItemEnum.Track),
- ).toBeUndefined();
- });
-
- test("returns the expected output when the requested item is present in categories", () => {
- expect(
- extractSessionCategoryInfo(categories, CategoryItemEnum.Level),
- ).toEqual("Introductory and overview ⭐");
- });
-
- test("returns the expected output when the requested item is present in categories with a different name", () => {
- expect(
- extractSessionCategoryInfo(categories, CategoryItemEnum.Language),
- ).toEqual("English 🇬🇧");
- });
-});
-
-describe("Fetch Talks by id", () => {
- beforeAll(() => {
- jest.mock("axios");
- });
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
- it("fetches and returns talks data for a specific id", async () => {
- const payload: AxiosResponse = {
- status: 200,
- statusText: "OK",
- headers: {},
- config: {
- headers: axiosHeaders,
- },
- data: {
- id: faker.number.int(),
- title: faker.lorem.text(),
- description: faker.lorem.lines(1),
- startsAt: faker.date.past().toString(),
- endsAt: faker.date.past().toString(),
- slidesURL: faker.internet.url(),
- speakers: [
- {
- id: faker.string.uuid(),
- name: faker.person.fullName(),
- },
- ],
- categories: [
- {
- id: 123,
- name: CategoryItemEnum.Level,
- categoryItems: [
- {
- id: faker.number.int(),
- name: faker.lorem.words(1),
- },
- ],
- },
- ],
- questionAnswers: [
- {
- id: 123,
- question: "",
- questionType: "",
- answer: "",
- },
- ],
- recordingUrl: "",
- track: "",
- },
- };
-
- mockedAxios.get.mockImplementation(() => Promise.resolve(payload));
-
- const wrapper: FC>> = ({
- children,
- }) => {
- return (
-
- {children}
-
- );
- };
-
- const {result} = renderHook(() => useFetchTalksById("1234"), {
- wrapper,
- });
-
- await waitFor(() => result.current.isSuccess);
- await waitFor(() => !result.current.isLoading);
- expect(mockedAxios.get).toHaveBeenNthCalledWith(
- 1,
- "https://sessionize.com/api/v2/teq4asez/view/Sessions",
- );
- expect(mockedAxios.get).toHaveReturnedTimes(1);
- //expect(result.current.isLoading).toEqual(false);
- expect(result.current.error).toEqual(null);
- //expect(result.current.data).toEqual(sessionAdapter(payload.data));
- });
-});
-
-describe("Fetch Live session talks", () => {
- afterEach(() => {
- jest.clearAllMocks();
- queryClient.clear();
- });
-
- it.skip("fetches and returns ungrouped talks data", async () => {
- const payload: AxiosResponse = {
- status: 200,
- statusText: "OK",
- headers: {},
- config: {
- headers: axiosHeaders,
- },
- data: {
- id: faker.string.uuid(),
- title: faker.lorem.lines(1),
- description: faker.lorem.lines(2),
- startsAt: faker.date.past().toLocaleString(),
- endsAt: faker.date.past().toLocaleString(),
- isConfirmed: true,
- isInformed: true,
- isPlenumSession: false,
- liveURL: null,
- isServiceSession: false,
- status: "Accepted",
- room: "Main Stage",
- roomID: faker.number.int(),
- questionAnswers: [],
- recordingURL: null,
- categories: [
- {
- id: faker.number.int(),
- name: "Session format",
- sort: 0,
- categoryItems: [],
- },
- ],
- speakers: [
- {
- id: faker.string.uuid(),
- name: faker.person.fullName(),
- },
- ],
- },
- };
-
- mockedAxios.get.mockResolvedValue(payload);
-
- const {result} = renderHook(() => useFetchLiveView(), {
- wrapper,
- });
-
- await waitFor(() => {
- expect(result.current.isSuccess).toBe(true);
- });
-
- expect(mockedAxios.get).toHaveBeenCalledWith(
- "https://sessionize.com/api/v2/ezm48alx/view/Sessions",
- );
- //expect(result.current.data).toStrictEqual(payload.data);
- expect(result.current.error).toBeNull();
- });
-});
diff --git a/src/hooks/useFetchTalks.test.tsx b/src/hooks/useFetchTalks.test.tsx
new file mode 100644
index 000000000..d957fc35c
--- /dev/null
+++ b/src/hooks/useFetchTalks.test.tsx
@@ -0,0 +1,371 @@
+import React, { FC } from "react";
+import { QueryClient, QueryClientProvider } from "react-query";
+import { renderHook, waitFor } from "@testing-library/react";
+import axios, { AxiosHeaders, AxiosResponse } from "axios";
+import {
+ useFetchLiveView,
+ useFetchTalks,
+ useFetchTalksById,
+} from "./useFetchTalks";
+import { Liveview } from "../views/Talks/liveView.types";
+import { CategoryItemEnum, IGroup } from "../types/sessions";
+
+jest.mock("axios");
+const mockedAxios = axios as jest.Mocked;
+const axiosHeaders = new AxiosHeaders();
+const queryClient = new QueryClient();
+const wrapper: FC>> = ({
+ children,
+}) => (
+ {children}
+);
+
+describe("useFetchTalks", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ queryClient.clear();
+ });
+
+ it("should use default URL when no parameter is provided", async () => {
+ const mockData: IGroup[] = [
+ {
+ groupId: 1,
+ groupName: "",
+ sessions: [],
+ isDefault: false,
+ },
+ ];
+ const payload: AxiosResponse = {
+ status: 200,
+ statusText: "OK",
+ headers: {},
+ config: {
+ headers: axiosHeaders,
+ },
+ data: mockData,
+ };
+
+ mockedAxios.get.mockResolvedValue(payload);
+
+ const { result } = renderHook(() => useFetchTalks(), {
+ wrapper,
+ });
+
+ await waitFor(() => result.current.isSuccess);
+ await waitFor(() => !result.current.isLoading);
+
+ expect(mockedAxios.get).toHaveBeenCalledWith(
+ "https://sessionize.com/api/v2/xhudniix/view/Sessions",
+ );
+ expect(result.current.data).toEqual(mockData);
+ });
+
+ it("should use 2023 URL when '2023' is provided", async () => {
+ const mockData: IGroup[] = [
+ {
+ groupId: 1,
+ sessions: [],
+ groupName: "test ",
+ isDefault: false,
+ },
+ ];
+ const payload: AxiosResponse = {
+ status: 200,
+ statusText: "OK",
+ headers: {},
+ config: {
+ headers: axiosHeaders,
+ },
+ data: mockData,
+ };
+
+ mockedAxios.get.mockResolvedValue(payload);
+
+ const { result } = renderHook(() => useFetchTalks("2023"), {
+ wrapper,
+ });
+
+ await waitFor(() => result.current.isSuccess);
+ await waitFor(() => !result.current.isLoading);
+
+ expect(mockedAxios.get).toHaveBeenCalledWith(
+ "https://sessionize.com/api/v2/ttsitynd/view/Sessions",
+ );
+ expect(result.current.data).toEqual(mockData);
+ });
+
+ it("should use 2024 URL when '2024' is provided", async () => {
+ const mockData: IGroup[] = [
+ {
+ groupId: 1,
+ groupName: "test",
+ isDefault: false,
+ sessions: [],
+ },
+ ];
+ const payload: AxiosResponse = {
+ status: 200,
+ statusText: "OK",
+ headers: {},
+ config: {
+ headers: axiosHeaders,
+ },
+ data: mockData,
+ };
+
+ mockedAxios.get.mockResolvedValue(payload);
+
+ const { result } = renderHook(() => useFetchTalks("2024"), {
+ wrapper,
+ });
+
+ await waitFor(() => result.current.isSuccess);
+ await waitFor(() => !result.current.isLoading);
+
+ expect(mockedAxios.get).toHaveBeenCalledWith(
+ "https://sessionize.com/api/v2/teq4asez/view/Sessions",
+ );
+ expect(result.current.data).toEqual(mockData);
+ });
+
+ it("should use custom URL when a URL is provided", async () => {
+ const mockData: IGroup[] = [
+ {
+ groupId: 1,
+ groupName: "test",
+ isDefault: false,
+ sessions: [],
+ },
+ ];
+ const payload: AxiosResponse = {
+ status: 200,
+ statusText: "OK",
+ headers: {},
+ config: {
+ headers: axiosHeaders,
+ },
+ data: mockData,
+ };
+
+ mockedAxios.get.mockResolvedValue(payload);
+
+ const customUrl = "https://example.com/api/sessions";
+ const { result } = renderHook(() => useFetchTalks(customUrl), {
+ wrapper,
+ });
+
+ await waitFor(() => result.current.isSuccess);
+ await waitFor(() => !result.current.isLoading);
+
+ expect(mockedAxios.get).toHaveBeenCalledWith(customUrl);
+ expect(result.current.data).toEqual(mockData);
+ });
+});
+
+describe("useFetchTalksById", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ queryClient.clear();
+ });
+
+ it("should use default URL when no parameter is provided", async () => {
+ const mockData: IGroup[] = [
+ {
+ groupId: 1,
+ groupName: "test",
+ isDefault: false,
+ sessions: [
+ {
+ id: 123,
+ title: "Test Session",
+ description: "Test Description",
+ endsAt: "2023-01-01T00:00:00Z",
+ startsAt: "2023-01-01T00:00:00Z",
+ track: "Test Track",
+ categories: [
+ {
+ id: 1,
+ name: CategoryItemEnum.Format,
+ categoryItems: [{ id: 1, name: "test category" }],
+ },
+ ],
+ speakers: [
+ {
+ id: "1",
+ name: "Test Speaker",
+ },
+ ],
+ questionAnswers: [
+ {
+ id: 1,
+ question: "Test Question",
+ answer: "Test Answer",
+ questionType: "text",
+ },
+ ],
+ },
+ ],
+ },
+ ];
+ const payload: AxiosResponse = {
+ status: 200,
+ statusText: "OK",
+ headers: {},
+ config: {
+ headers: axiosHeaders,
+ },
+ data: mockData,
+ };
+
+ mockedAxios.get.mockResolvedValue(payload);
+
+ const { result } = renderHook(() => useFetchTalksById("123"), {
+ wrapper,
+ });
+
+ await waitFor(() => result.current.isSuccess);
+ await waitFor(() => !result.current.isLoading);
+
+ expect(mockedAxios.get).toHaveBeenCalledWith(
+ "https://sessionize.com/api/v2/xhudniix/view/Sessions",
+ );
+ const expectedData = mockData[0].sessions[0];
+ expect(result.current.data).toEqual(expectedData);
+ });
+
+ it("should use 2023 URL when '2023' is provided", async () => {
+ const mockData: IGroup[] = [
+ {
+ groupId: 1,
+ groupName: "test ",
+ isDefault: false,
+ sessions: [
+ {
+ id: 123,
+ title: "Test Session",
+ track: "",
+ description: "",
+ endsAt: "2023-01-01T00:00:00Z",
+ startsAt: "2023-01-01T00:00:00",
+ categories: [
+ {
+ id: 1,
+ name: CategoryItemEnum.Format,
+ categoryItems: [{ id: 1, name: "test category" }],
+ },
+ ],
+ speakers: [
+ {
+ id: "1",
+ name: "Test Speaker",
+ },
+ ],
+ questionAnswers: [
+ {
+ id: 1,
+ question: "Test Question",
+ answer: "Test Answer",
+ questionType: "text",
+ },
+ ],
+ },
+ ],
+ },
+ ];
+ const payload: AxiosResponse = {
+ status: 200,
+ statusText: "OK",
+ headers: {},
+ config: {
+ headers: axiosHeaders,
+ },
+ data: mockData,
+ };
+
+ mockedAxios.get.mockResolvedValue(payload);
+
+ const { result } = renderHook(() => useFetchTalksById("123", "2023"), {
+ wrapper,
+ });
+
+ await waitFor(() => result.current.isSuccess);
+ await waitFor(() => !result.current.isLoading);
+
+ expect(mockedAxios.get).toHaveBeenCalledWith(
+ "https://sessionize.com/api/v2/ttsitynd/view/Sessions",
+ );
+ const expectedData = mockData[0].sessions[0];
+ expect(result.current.data).toEqual(expectedData);
+ });
+});
+
+describe("useFetchLiveView", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ queryClient.clear();
+ });
+
+ it("should use default URL when no parameter is provided", async () => {
+ const mockData: Liveview = {
+ groupName: "",
+ groupID: null,
+ isDefault: false,
+ sessions: [],
+ };
+ const payload: AxiosResponse = {
+ status: 200,
+ statusText: "OK",
+ headers: {},
+ config: {
+ headers: axiosHeaders,
+ },
+ data: [mockData],
+ };
+
+ mockedAxios.get.mockResolvedValue(payload);
+
+ const { result } = renderHook(() => useFetchLiveView(), {
+ wrapper,
+ });
+
+ await waitFor(() => result.current.isSuccess);
+ await waitFor(() => !result.current.isLoading);
+
+ expect(mockedAxios.get).toHaveBeenCalledWith(
+ "https://sessionize.com/api/v2/xhudniix/view/Sessions",
+ );
+ expect(result.current.data).toEqual(payload.data[0]);
+ });
+
+ it("should use 2024 URL when '2024' is provided", async () => {
+ const mockData: Liveview = {
+ groupID: null,
+ groupName: "",
+ isDefault: false,
+ sessions: [],
+ };
+ const payload: AxiosResponse = {
+ status: 200,
+ statusText: "OK",
+ headers: {},
+ config: {
+ headers: axiosHeaders,
+ },
+ data: [mockData],
+ };
+
+ mockedAxios.get.mockResolvedValue(payload);
+
+ const { result } = renderHook(() => useFetchLiveView("2024"), {
+ wrapper,
+ });
+
+ await waitFor(() => result.current.isSuccess);
+ await waitFor(() => !result.current.isLoading);
+
+ expect(mockedAxios.get).toHaveBeenCalledWith(
+ "https://sessionize.com/api/v2/teq4asez/view/Sessions",
+ );
+ expect(result.current.data).toEqual(payload.data[0]);
+ });
+});
diff --git a/src/hooks/useFetchTalks.ts b/src/hooks/useFetchTalks.ts
new file mode 100644
index 000000000..b25df6d9c
--- /dev/null
+++ b/src/hooks/useFetchTalks.ts
@@ -0,0 +1,77 @@
+import { useQuery, UseQueryResult } from "react-query";
+import axios from "axios";
+import { Liveview } from "../views/Talks/liveView.types";
+import { IGroup, Session } from "../types/sessions";
+
+const URLS = {
+ default: "https://sessionize.com/api/v2/xhudniix/view/Sessions",
+ 2023: "https://sessionize.com/api/v2/ttsitynd/view/Sessions",
+ 2024: "https://sessionize.com/api/v2/teq4asez/view/Sessions",
+};
+
+/**
+ * Determines the URL to use based on the urlOrYear parameter
+ * @param urlOrYear - Optional URL or year to use
+ * @returns The URL to use
+ */
+const getUrl = (urlOrYear?: string): string => {
+ let url = URLS.default;
+
+ if (urlOrYear) {
+ // If urlOrYear starts with http, it's a URL
+ if (urlOrYear.startsWith("http")) {
+ url = urlOrYear;
+ }
+ // If urlOrYear is a year key in URLS, use that URL
+ else if (urlOrYear in URLS) {
+ url = URLS[urlOrYear as keyof typeof URLS];
+ }
+ }
+
+ return url;
+};
+
+/**
+ * Base hook for fetching talks data
+ * @param queryKey - The query key to use
+ * @param urlOrYear - Optional URL or year to use
+ * @param dataTransformer - Function to transform the response data
+ * @returns The query result
+ */
+const useFetchTalksBase = (
+ queryKey: string,
+ urlOrYear?: string,
+ dataTransformer: (data: any) => T = (data) => data,
+): UseQueryResult => {
+ const url = getUrl(urlOrYear);
+
+ return useQuery(queryKey, async () => {
+ const response = await axios.get(url);
+ return dataTransformer(response.data);
+ });
+};
+
+export const useFetchTalks = (urlOrYear?: string): UseQueryResult => {
+ return useFetchTalksBase("api-talks", urlOrYear);
+};
+
+export const useFetchTalksById = (
+ id: string,
+ urlOrYear?: string,
+): UseQueryResult => {
+ return useFetchTalksBase("talks", urlOrYear, (data) => {
+ const sessions = data
+ .map((track: IGroup) => track.sessions)
+ .flat(1)
+ .filter((session: { id: number | string }) => String(session.id) === id);
+ return sessions[0];
+ });
+};
+
+export const useFetchLiveView = (
+ urlOrYear?: string,
+): UseQueryResult => {
+ return useFetchTalksBase("api-talks", urlOrYear, (data) =>
+ data.at(0),
+ );
+};
diff --git a/src/views/MeetingDetail/TalkDetailContainer2024.tsx b/src/views/MeetingDetail/TalkDetailContainer2024.tsx
index 496a1e1dd..63e347d05 100644
--- a/src/views/MeetingDetail/TalkDetailContainer2024.tsx
+++ b/src/views/MeetingDetail/TalkDetailContainer2024.tsx
@@ -1,17 +1,17 @@
-import {Color} from "../../styles/colors";
-import React, {FC, useEffect} from "react";
+import { Color } from "../../styles/colors";
+import React, { FC, useEffect } from "react";
import NotFoundError from "../../components/NotFoundError/NotFoundError";
import SectionWrapper from "../../components/SectionWrapper/SectionWrapper";
import styled from "styled-components";
-import {useParams} from "react-router";
+import { useParams } from "react-router";
import conferenceData from "../../data/2025.json";
-import {useFetchTalksById} from "../Talks/UseFetchTalks";
+import { useFetchTalksById } from "../../hooks/useFetchTalks";
import * as Sentry from "@sentry/react";
-import {useFetchSpeakers} from "../../hooks/useFetchSpeakers";
+import { useFetchSpeakers } from "../../hooks/useFetchSpeakers";
import MeetingDetail from "./MeetingDetail";
-import {ISpeaker} from "../../types/speakers";
-import {sessionAdapter} from "../../services/sessionsAdapter";
-import {Session} from "../../types/sessions";
+import { ISpeaker } from "../../types/speakers";
+import { sessionAdapter } from "../../services/sessionsAdapter";
+import { Session } from "../../types/sessions";
const StyledContainer = styled.div`
background-color: ${Color.WHITE};
diff --git a/src/views/Talks/LiveView.tsx b/src/views/Talks/LiveView.tsx
index d54078abb..818a1c5f0 100644
--- a/src/views/Talks/LiveView.tsx
+++ b/src/views/Talks/LiveView.tsx
@@ -1,5 +1,5 @@
import React, { FC, useCallback, useEffect, useMemo } from "react";
-import { useFetchLiveView } from "./UseFetchTalks";
+import { useFetchLiveView } from "../../hooks/useFetchTalks";
import Loading from "../../components/Loading/Loading";
import { UngroupedSession } from "./liveView.types";
import conference from "../../data/2024.json";
diff --git a/src/views/Talks/Talks.tsx b/src/views/Talks/Talks.tsx
index 8682517c5..e767dbbab 100644
--- a/src/views/Talks/Talks.tsx
+++ b/src/views/Talks/Talks.tsx
@@ -13,7 +13,7 @@ import {
StyledWaveContainer,
} from "./Talks.style";
import TrackInformation from "./components/TrackInformation";
-import { useFetchTalks } from "./UseFetchTalks";
+import { useFetchTalks } from "../../hooks/useFetchTalks";
import * as Sentry from "@sentry/react";
import { Dropdown, DropdownChangeEvent } from "primereact/dropdown";
import "primereact/resources/primereact.min.css";
diff --git a/src/views/Talks/UseFetchTalks.ts b/src/views/Talks/UseFetchTalks.ts
deleted file mode 100644
index f11cedd98..000000000
--- a/src/views/Talks/UseFetchTalks.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import {useQuery, UseQueryResult} from "react-query";
-import axios from "axios";
-import {Liveview} from "./liveView.types";
-import {IGroup, Session} from "../../types/sessions";
-
-export const useFetchTalks = (): UseQueryResult =>
- useQuery("api-talks", async () => {
- let data = await axios.get(
- "https://sessionize.com/api/v2/xhudniix/view/Sessions",
- );
- return data.data;
- });
-
-export const useFetchTalksById = (id: string): UseQueryResult =>
- useQuery("talks", async () => {
- const serverResponse = await axios.get(
- "https://sessionize.com/api/v2/xhudniix/view/Sessions",
- );
- return serverResponse.data
- .map((track: IGroup) => track.sessions)
- .flat(1)
- .filter((session: { id: string }) => session.id === id);
- });
-
-export const useFetchLiveView = (): UseQueryResult =>
- useQuery("api-talks", async () => {
- let data = await axios.get(
- "https://sessionize.com/api/v2/xhudniix/view/Sessions",
- );
- return data.data.at(0);
- });
-
diff --git a/src/views/Talks/useFetchTalks.test.tsx b/src/views/Talks/useFetchTalks.test.tsx
deleted file mode 100644
index 94449a72e..000000000
--- a/src/views/Talks/useFetchTalks.test.tsx
+++ /dev/null
@@ -1,422 +0,0 @@
-import React, {FC} from "react";
-import {QueryClient, QueryClientProvider} from "react-query";
-import {renderHook, waitFor} from "@testing-library/react";
-import axios, {AxiosHeaders, AxiosResponse} from "axios";
-import {faker} from "@faker-js/faker";
-import {useFetchLiveView, useFetchTalksById,} from "./UseFetchTalks";
-import {UngroupedSession} from "./liveView.types";
-import {
- extractSessionCategoryInfo,
- extractSessionSlides,
- extractSessionTags,
- sessionAdapter
-} from "../../services/sessionsAdapter";
-import {
- CategoryItemEnum,
- IMeeting,
- QuestionAnswers,
- Session,
- SessionCategory
-} from "../../types/sessions";
-
-jest.mock("axios");
-const mockedAxios = axios as jest.Mocked;
-const axiosHeaders = new AxiosHeaders();
-const queryClient = new QueryClient();
-const wrapper: FC>> = ({
- children,
-}) => (
- {children}
-);
-
-describe("sessionAdapter", () => {
- test("returns empty strings when session is undefined", () => {
- expect(sessionAdapter(undefined)).toBeUndefined();
- });
-
- test("returns the expected output when session is defined", () => {
- const session: Session = {
- track: "Java ( core frameworks & libraries )",
- id: 5000,
- description: "Session description",
- startsAt: "2024-06-13T12:00:00",
- endsAt: "2024-06-13T14:00:00",
- title: "Session title",
- speakers: [
- {
- id: "6f672350-1c71-4a6e-a382-2b1db6e631fd",
- name: "Eric Deandrea",
- },
- {
- id: "4452d53b-603f-4185-beab-766a19258c0f",
- name: "Holly Cummins",
- },
- ],
- recordingUrl: "https://example.com/video.mp4",
- questionAnswers: [
- {
- id: 47395,
- question: "Tags/Topics",
- questionType: "Short_Text",
- answer: "java,openjdk",
- },
- {
- id: 3425,
- question: "Slides",
- questionType: "web_address",
- answer: "https://www.google.com",
- },
- ],
- categories: [
- {
- id: 45078,
- name: CategoryItemEnum.Format,
- categoryItems: [
- {
- id: 149212,
- name: "Session",
- },
- ],
- },
- {
- id: 45079,
- name: CategoryItemEnum.Track,
- categoryItems: [
- {
- id: 159116,
- name: "Java ( core frameworks & libraries )",
- },
- ],
- },
- {
- id: 45080,
- name: CategoryItemEnum.Level,
- categoryItems: [
- {
- id: 149217,
- name: "Introductory and overview",
- },
- ],
- },
- {
- id: 45081,
- name: CategoryItemEnum.Language,
- categoryItems: [
- {
- id: 149221,
- name: "English",
- },
- ],
- },
- ],
- };
- const expected: IMeeting = {
- id: 5000,
- description: "Session description",
- title: "Session title",
- speakers: [
- {
- id: "6f672350-1c71-4a6e-a382-2b1db6e631fd",
- name: "Eric Deandrea",
- },
- {
- id: "4452d53b-603f-4185-beab-766a19258c0f",
- name: "Holly Cummins",
- },
- ],
- videoUrl: "https://example.com/video.mp4",
- slidesURL: "https://www.google.com",
- videoTags: ["java", "openjdk"],
- level: "Introductory and overview ⭐",
- language: "English 🇬🇧",
- type: "Session 🗣",
- track: "Java ( core frameworks & libraries )",
- startDate: "2024-06-13",
- startTime: "12:00:00",
- endDate: "2024-06-13",
- endTime: "14:00:00",
- };
-
- expect(sessionAdapter(session)).toEqual(expected);
- });
-});
-
-describe("extractSessionTags", () => {
- test("returns undefined when questionAnswers is empty", () => {
- expect(extractSessionTags([])).toBeUndefined();
- });
-
- test("returns undefined when questionAnswers do not have a Tags/Topics question", () => {
- const questionAnswers: QuestionAnswers[] = [
- {
- id: 45775,
- question: "Question 1",
- answer: "Answer 1",
- questionType: "Short_Text",
- },
- {
- id: 999,
- question: "Question 2",
- answer: "Answer 2",
- questionType: "Short_Text",
- },
- ];
-
- expect(extractSessionTags(questionAnswers)).toBeUndefined();
- });
-
- test("returns the expected output when questionAnswers have a Tags/Topics question", () => {
- const questionAnswers: QuestionAnswers[] = [
- {
- id: 1,
- question: "Question 1",
- answer: "Answer 1",
- questionType: "Short_Text",
- },
- {
- id: 2,
- question: "Tags/Topics",
- answer: "tag1, tag2, tag3",
- questionType: "Short_Text",
- },
- {
- id: 3,
- question: "Question 2",
- answer: "Answer 2",
- questionType: "Short_Text",
- },
- ];
-
- expect(extractSessionTags(questionAnswers)).toEqual([
- "tag1",
- " tag2",
- " tag3",
- ]);
- });
-});
-
-describe("extractSessionSlides", () => {
- test("returns empty when questionAnswers is empty", () => {
- expect(extractSessionSlides([])).toEqual("");
- });
-
- test("returns the expected output when questionAnswers have a Slides question", () => {
- const questionAnswers: QuestionAnswers[] = [
- {
- id: 1,
- question: "Question 1",
- answer: "Answer 1",
- questionType: "Short_Text",
- },
- {
- id: 2,
- question: "Slides",
- answer: "https://www.google.com",
- questionType: "Short_Text",
- },
- {
- id: 3,
- question: "Question 2",
- answer: "Answer 2",
- questionType: "Short_Text",
- },
- ];
-
- expect(extractSessionSlides(questionAnswers)).toEqual(
- "https://www.google.com",
- );
- });
-});
-
-describe("extractSessionCategoryInfo", () => {
- const categories: SessionCategory[] = [
- {
- id: 4,
- name: CategoryItemEnum.Level,
- categoryItems: [
- { id: 1, name: "Introductory and overview" },
- { id: 2, name: "Intermediate" },
- ],
- },
- {
- id: 8,
- name: CategoryItemEnum.Language,
- categoryItems: [
- { id: 3, name: "English" },
- { id: 4, name: "Spanish" },
- ],
- },
- ];
-
- test("returns undefined when categories is empty", () => {
- expect(
- extractSessionCategoryInfo([], CategoryItemEnum.Level),
- ).toBeUndefined();
- });
-
- test("returns undefined when the requested item is not present in categories", () => {
- expect(
- extractSessionCategoryInfo(categories, CategoryItemEnum.Track),
- ).toBeUndefined();
- });
-
- test("returns the expected output when the requested item is present in categories", () => {
- expect(
- extractSessionCategoryInfo(categories, CategoryItemEnum.Level),
- ).toEqual("Introductory and overview ⭐");
- });
-
- test("returns the expected output when the requested item is present in categories with a different name", () => {
- expect(
- extractSessionCategoryInfo(categories, CategoryItemEnum.Language),
- ).toEqual("English 🇬🇧");
- });
-});
-
-describe("Fetch Talks by id", () => {
- beforeAll(() => {
- jest.mock("axios");
- });
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
- it("fetches and returns talks data for a specific id", async () => {
- const payload: AxiosResponse = {
- status: 200,
- statusText: "OK",
- headers: {},
- config: {
- headers: axiosHeaders,
- },
- data: {
- id: faker.number.int(),
- title: faker.lorem.text(),
- description: faker.lorem.lines(1),
- startsAt: faker.date.past().toString(),
- endsAt: faker.date.past().toString(),
- slidesURL: faker.internet.url(),
- speakers: [
- {
- id: faker.string.uuid(),
- name: faker.person.fullName(),
- },
- ],
- categories: [
- {
- id: 123,
- name: CategoryItemEnum.Level,
- categoryItems: [
- {
- id: faker.number.int(),
- name: faker.lorem.words(1),
- },
- ],
- },
- ],
- questionAnswers: [
- {
- id: 123,
- question: "",
- questionType: "",
- answer: "",
- },
- ],
- recordingUrl: "",
- track: "",
- },
- };
-
- mockedAxios.get.mockImplementation(() => Promise.resolve(payload));
-
- const wrapper: FC>> = ({
- children,
- }) => {
- return (
-
- {children}
-
- );
- };
-
- const { result } = renderHook(() => useFetchTalksById("1234"), {
- wrapper,
- });
-
- await waitFor(() => result.current.isSuccess);
- await waitFor(() => !result.current.isLoading);
- expect(mockedAxios.get).toHaveBeenNthCalledWith(
- 1,
- "https://sessionize.com/api/v2/xhudniix/view/Sessions",
- );
- expect(mockedAxios.get).toHaveReturnedTimes(1);
- //expect(result.current.isLoading).toEqual(false);
- expect(result.current.error).toEqual(null);
- //expect(result.current.data).toEqual(sessionAdapter(payload.data));
- });
-});
-
-describe("Fetch Live session talks", () => {
- afterEach(() => {
- jest.clearAllMocks();
- queryClient.clear();
- });
-
- it.skip("fetches and returns ungrouped talks data", async () => {
- const payload: AxiosResponse = {
- status: 200,
- statusText: "OK",
- headers: {},
- config: {
- headers: axiosHeaders,
- },
- data: {
- id: faker.string.uuid(),
- title: faker.lorem.lines(1),
- description: faker.lorem.lines(2),
- startsAt: faker.date.past().toLocaleString(),
- endsAt: faker.date.past().toLocaleString(),
- isConfirmed: true,
- isInformed: true,
- isPlenumSession: false,
- liveURL: null,
- isServiceSession: false,
- status: "Accepted",
- room: "Main Stage",
- roomID: faker.number.int(),
- questionAnswers: [],
- recordingURL: null,
- categories: [
- {
- id: faker.number.int(),
- name: "Session format",
- sort: 0,
- categoryItems: [],
- },
- ],
- speakers: [
- {
- id: faker.string.uuid(),
- name: faker.person.fullName(),
- },
- ],
- },
- };
-
- mockedAxios.get.mockResolvedValue(payload);
-
- const { result } = renderHook(() => useFetchLiveView(), {
- wrapper,
- });
-
- await waitFor(() => {
- expect(result.current.isSuccess).toBe(true);
- });
-
- expect(mockedAxios.get).toHaveBeenCalledWith(
- "https://sessionize.com/api/v2/ezm48alx/view/Sessions",
- );
- //expect(result.current.data).toStrictEqual(payload.data);
- expect(result.current.error).toBeNull();
- });
-});
diff --git a/src/views/Workshops/Workshops.tsx b/src/views/Workshops/Workshops.tsx
index d9b911b34..7c9bcb609 100644
--- a/src/views/Workshops/Workshops.tsx
+++ b/src/views/Workshops/Workshops.tsx
@@ -11,7 +11,7 @@ import {
import LessThanDarkBlueIcon from "../../assets/images/LessThanDarkBlueIcon.svg";
import TitleSection from "../../components/SectionTitle/TitleSection";
import MoreThanBlueIcon from "../../assets/images/MoreThanBlueIcon.svg";
-import { useFetchTalks } from "../Talks/UseFetchTalks";
+import { useFetchTalks } from "../../hooks/useFetchTalks";
import * as Sentry from "@sentry/react";
import { TalkCard } from "../Talks/components/TalkCard";
import conferenceData from "../../data/2025.json";
@@ -19,21 +19,21 @@ import styled from "styled-components";
import { BIG_BREAKPOINT } from "../../constants/BreakPoints";
const StyledSection = styled.section`
- {
+{
display: flex;
padding: 0 10rem;
flex-wrap: wrap;
- }
+}
- @media (max-width: ${BIG_BREAKPOINT}px) {
- padding: 1rem;
- flex-direction: column;
- }
+ @media (max-width: ${BIG_BREAKPOINT}px) {
+ padding: 1rem;
+ flex-direction: column;
+ }
- & > div {
- margin: 1rem;
- min-width: 14%;
- }
+ & > div {
+ margin: 1rem;
+ min-width: 14%;
+ }
`;
const Workshops: FC> = () => {
const { isLoading, data, error } = useFetchTalks();
From 1cd97fdcf6a672e18841f771cfa6d905f87efd1a Mon Sep 17 00:00:00 2001
From: Anyul Rivas
Date: Tue, 25 Mar 2025 09:00:12 +0100
Subject: [PATCH 04/11] test: fix tests
---
src/hooks/useFetchSpeakers.ts | 25 ++++++++++++++-----------
1 file changed, 14 insertions(+), 11 deletions(-)
diff --git a/src/hooks/useFetchSpeakers.ts b/src/hooks/useFetchSpeakers.ts
index 94e0b2fe5..2f1cf9dbd 100644
--- a/src/hooks/useFetchSpeakers.ts
+++ b/src/hooks/useFetchSpeakers.ts
@@ -6,26 +6,29 @@ import { ISpeaker } from "../types/speakers";
const URLS = {
default: "https://sessionize.com/api/v2/xhudniix/view/Speakers",
2023: "https://sessionize.com/api/v2/ttsitynd/view/Speakers",
- 2024: "https://sessionize.com/api/v2/teq4asez/view/Speakers"
+ 2024: "https://sessionize.com/api/v2/teq4asez/view/Speakers",
};
-export const useFetchSpeakers = (yearOrUrl?: string, id?: string): UseQueryResult => {
+export const useFetchSpeakers = (
+ yearOrUrl?: string,
+ id?: string,
+): UseQueryResult => {
// Determine if the first parameter is a URL or an ID
let url = URLS.default;
let speakerId = id;
- if (urlOrId) {
+ if (yearOrUrl) {
// If urlOrId starts with http, it's a URL
- if (urlOrId.startsWith("http")) {
- url = urlOrId;
- }
+ if (yearOrUrl.startsWith("http")) {
+ url = yearOrUrl;
+ }
// If urlOrId is a year key in URLS, use that URL
- else if (urlOrId in URLS) {
- url = URLS[urlOrId as keyof typeof URLS];
- }
+ else if (yearOrUrl in URLS) {
+ url = URLS[yearOrUrl as keyof typeof URLS];
+ }
// Otherwise, it's an ID
else {
- speakerId = urlOrId;
+ speakerId = yearOrUrl;
}
}
@@ -42,4 +45,4 @@ export const useFetchSpeakers = (yearOrUrl?: string, id?: string): UseQueryResul
return speakerAdapter(returnData);
});
-};
\ No newline at end of file
+};
From b3aefd7b518797681a8a604b84b529e6773368be Mon Sep 17 00:00:00 2001
From: Anyul Rivas
Date: Tue, 25 Mar 2025 09:05:47 +0100
Subject: [PATCH 05/11] chore: spell check
---
wordlist.txt | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/wordlist.txt b/wordlist.txt
index acea55813..b073bfceb 100644
--- a/wordlist.txt
+++ b/wordlist.txt
@@ -1,6 +1,7 @@
BvU
CFP
Cfp
+dataTransformer
DOM
DevBCN
DevBcn
@@ -45,9 +46,11 @@ minified
nd
npm
onChange
+param
pbs
png
px
+queryKey
rd
robotstxt
selectedGroupId
@@ -56,6 +59,7 @@ svg
th
toHaveTextContent
twimg
+urlOrYear
veepee
vilojona
webfonts
From 216403df9d5d1d1ef1a895b38563577c1394828e Mon Sep 17 00:00:00 2001
From: Anyul Rivas
Date: Tue, 25 Mar 2025 09:13:01 +0100
Subject: [PATCH 06/11] chore: spell check
---
wordlist.txt | 1 +
1 file changed, 1 insertion(+)
diff --git a/wordlist.txt b/wordlist.txt
index b073bfceb..fe1378549 100644
--- a/wordlist.txt
+++ b/wordlist.txt
@@ -60,6 +60,7 @@ th
toHaveTextContent
twimg
urlOrYear
+urlOrId
veepee
vilojona
webfonts
From d6e0d997c8c8fdeb8e305b40ce3010efbd4b7d3b Mon Sep 17 00:00:00 2001
From: Anyul Rivas
Date: Tue, 25 Mar 2025 09:19:19 +0100
Subject: [PATCH 07/11] docs: updated README.md
---
.junie/guidelines.md | 60 ++++++++++++++++++++++++++++++++++
README.md | 78 +++++++++++++++++++++++++++-----------------
2 files changed, 108 insertions(+), 30 deletions(-)
create mode 100644 .junie/guidelines.md
diff --git a/.junie/guidelines.md b/.junie/guidelines.md
new file mode 100644
index 000000000..d4048e3cd
--- /dev/null
+++ b/.junie/guidelines.md
@@ -0,0 +1,60 @@
+# DevBcn - Barcelona Developers Conference Website
+
+## Project Overview
+
+This repository contains the official website for the Barcelona Developers Conference (DevBcn), a tech conference held in Barcelona, Spain. The website serves as the primary platform for conference information, including schedules, speaker profiles, talk details, venue information, and registration.
+
+## Technology Stack
+
+- **Frontend Framework**: React 18 with TypeScript
+- **Routing**: React Router
+- **Data Fetching**: React Query and Axios
+- **Styling**: Styled Components and SASS
+- **UI Components**: PrimeReact, Swiper, Framer Motion
+- **Maps Integration**: Google Map React
+- **Testing**: Jest, React Testing Library
+- **Deployment**: GitHub Pages
+
+## Project Structure
+
+The project follows a standard React application structure:
+
+- `src/`: Source code
+ - `assets/`: Static assets like images
+ - `components/`: Reusable UI components
+ - `hooks/`: Custom React hooks (e.g., useFetchSpeakers, useFetchTalks)
+ - `views/`: Page components
+ - `2024/`: Components specific to the 2024 conference
+
+## Development Workflow
+
+### Getting Started
+
+1. Clone the repository
+2. Install dependencies with `npm install`
+3. Start the development server with `npm start`
+4. View the site at http://localhost:3000
+
+### Available Scripts
+
+- `npm start`: Run the development server
+- `npm test`: Run tests
+- `npm run test-coverage`: Run tests with coverage reporting
+- `npm run build`: Build for production
+- `npm run deploy`: Deploy to GitHub Pages
+
+## Contribution Guidelines
+
+When contributing to this project, please:
+
+1. Follow the existing code style and patterns
+2. Write tests for new features
+3. Ensure all tests pass before submitting pull requests
+4. Keep the UI consistent with the existing design
+5. Document any new components or significant changes
+
+## Contact
+
+For questions or issues related to the DevBcn website, please open an issue in this repository.
+
+Visit the live site at [https://www.devbcn.com](https://www.devbcn.com)
\ No newline at end of file
diff --git a/README.md b/README.md
index 5fa7004e4..745002787 100644
--- a/README.md
+++ b/README.md
@@ -2,51 +2,69 @@

-# Barcelona Developers Conference - DevBcn
+# DevBcn - Barcelona Developers Conference Website
-## Getting Started with Create React App
+## Project Overview
-This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
+This repository contains the official website for the Barcelona Developers
+Conference (DevBcn), a tech conference held in Barcelona, Spain. The website
+serves as the primary platform for conference information, including schedules,
+speaker profiles, talk details, venue information, and registration.
-### Available Scripts
-
-In the project directory, you can run:
-
-#### `npm start`
+## Technology Stack
-Runs the app in the development mode.\
-Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
+- **Frontend Framework**: React 18 with TypeScript
+- **Routing**: React Router
+- **Data Fetching**: React Query and Axios
+- **Styling**: Styled Components and SASS
+- **UI Components**: PrimeReact, Swiper, Framer Motion
+- **Maps Integration**: Google Map React
+- **Testing**: Jest, React Testing Library
+- **Deployment**: GitHub Pages
-The page will reload if you make edits.\
-You will also see any lint errors in the console.
+## Project Structure
-#### `npm test`
+The project follows a standard React application structure:
-Launches the test runner in the interactive watch mode.\
-See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
+- `src/`: Source code
+ - `assets/`: Static assets like images
+ - `components/`: Reusable UI components
+ - `hooks/`: Custom React hooks (e.g., useFetchSpeakers, useFetchTalks)
+ - `views/`: Page components
+ - `2024/`: Components specific to the 2024 conference edition
+ - `2023/`: Components specific to the 2023 conference edition
-#### `npm run build`
+## Development Workflow
-Builds the app for production to the `build` folder.\
-It correctly bundles React in production mode and optimizes the build for the best performance.
+### Getting Started
-The build is minified and the filenames include the hashes.\
-Your app is ready to be deployed!
+1. Clone the repository
+2. Install dependencies with `npm install`
+3. Start the development server with `npm start`
+4. View the site at http://localhost:3000
-See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
-
-#### `npm run eject`
+### Available Scripts
-**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
+- `npm start`: Run the development server
+- `npm test`: Run tests
+- `npm run test-coverage`: Run tests with coverage reporting
+- `npm run build`: Build for production
+- `npm run deploy`: Deploy to GitHub Pages
+- `npm run eject`: Eject from Create React App (not recommended)
-If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
+## Contribution Guidelines
-Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
+When contributing to this project, please:
-You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
+1. Follow the existing code style and patterns
+2. Write tests for new features
+3. Ensure all tests pass before submitting pull requests
+4. Keep the UI consistent with the existing design
+5. Document any new components or significant changes
-### Learn More
+## Contact
-You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
+For questions or issues related to the DevBcn website, please open an issue in
+this repository.
-To learn React, check out the [React documentation](https://reactjs.org/).
+Visit the live site at [https://www.devbcn.com](https://www.devbcn.com)
\ No newline at end of file
From 132166b50fe9fd379b97f743ac908afbb9a9bcc2 Mon Sep 17 00:00:00 2001
From: Anyul Rivas
Date: Tue, 25 Mar 2025 09:39:46 +0100
Subject: [PATCH 08/11] refactor: remove duplication on test files
---
src/2024/Talks/LiveView.test.tsx | 10 +-
src/2024/Talks/Talks.test.tsx | 46 +----
src/hooks/useFetchSpeakers.test.tsx | 200 +++++---------------
src/hooks/useFetchTalks.test.tsx | 275 ++++++----------------------
src/utils/testing/testUtils.tsx | 147 +++++++++++++++
src/views/Talks/LiveView.test.tsx | 10 +-
src/views/Talks/Talks.test.tsx | 46 +----
7 files changed, 263 insertions(+), 471 deletions(-)
create mode 100644 src/utils/testing/testUtils.tsx
diff --git a/src/2024/Talks/LiveView.test.tsx b/src/2024/Talks/LiveView.test.tsx
index cc65de352..a37faaa26 100644
--- a/src/2024/Talks/LiveView.test.tsx
+++ b/src/2024/Talks/LiveView.test.tsx
@@ -1,16 +1,10 @@
import LiveView from "./LiveView";
-import {QueryClient, QueryClientProvider} from "react-query";
-import {render, screen} from "@testing-library/react";
import React from "react";
+import { renderWithQueryClient, screen } from "../../utils/testing/testUtils";
describe("Live view component", () => {
it("renders without crashing", () => {
- const queryClient = new QueryClient();
- render(
-
-
- ,
- );
+ renderWithQueryClient();
const titleElement = screen.getByText(/Live Schedule/);
expect(titleElement).toBeInTheDocument();
});
diff --git a/src/2024/Talks/Talks.test.tsx b/src/2024/Talks/Talks.test.tsx
index 815f19a02..48e72f87b 100644
--- a/src/2024/Talks/Talks.test.tsx
+++ b/src/2024/Talks/Talks.test.tsx
@@ -1,36 +1,21 @@
import React from "react";
-import {render, screen} from "@testing-library/react";
+import { screen } from "@testing-library/react";
import Talks2024 from "./Talks2024";
-import {QueryClient, QueryClientProvider} from "react-query";
+import { renderWithQueryClient } from "../../utils/testing/testUtils";
describe("Talks", () => {
it("renders without errors", () => {
- const queryClient = new QueryClient();
- render(
-
-
-
- );
+ renderWithQueryClient();
});
it("renders the correct title", () => {
- const queryClient = new QueryClient();
- render(
-
-
-
- );
+ renderWithQueryClient();
const titleElement = screen.getByText(/TALKS/);
expect(titleElement).toBeInTheDocument();
});
it("renders the correct subtitle", () => {
- const queryClient = new QueryClient();
- render(
-
-
-
- );
+ renderWithQueryClient();
const subtitleElement = screen.getByText(
/speakers coming from all corners of the world/i
);
@@ -38,33 +23,18 @@ describe("Talks", () => {
});
it("renders a filter by track dropdown", () => {
- const queryClient = new QueryClient();
- render(
-
-
-
- );
+ renderWithQueryClient();
const dropdownElement = screen.getByText("Loading");
expect(dropdownElement).toBeInTheDocument();
});
it("renders a loading message when talks are being fetched", () => {
- const queryClient = new QueryClient();
- render(
-
-
-
- );
+ renderWithQueryClient();
expect(screen.getByText("Loading")).toBeInTheDocument();
});
it("renders a message when no talks are selected", () => {
- const queryClient = new QueryClient();
- render(
-
-
-
- );
+ renderWithQueryClient();
const dropdownElement = screen.getByText("Loading");
expect(dropdownElement).toBeInTheDocument();
});
diff --git a/src/hooks/useFetchSpeakers.test.tsx b/src/hooks/useFetchSpeakers.test.tsx
index b168d5c17..d94028174 100644
--- a/src/hooks/useFetchSpeakers.test.tsx
+++ b/src/hooks/useFetchSpeakers.test.tsx
@@ -1,70 +1,43 @@
-import React, {FC} from "react";
-import {QueryClient, QueryClientProvider} from "react-query";
-import {renderHook, waitFor} from "@testing-library/react";
-import {useFetchSpeakers} from "./useFetchSpeakers";
-import axios, {AxiosHeaders, AxiosResponse} from "axios";
-import {speakerAdapter} from "../services/speakerAdapter";
-import {IResponse} from "../types/speakers";
+import { renderHook, waitFor } from "@testing-library/react";
+import { useFetchSpeakers } from "./useFetchSpeakers";
+import axios from "axios";
+import { speakerAdapter } from "../services/speakerAdapter";
+import { IResponse } from "../types/speakers";
+import {
+ createMockAxiosResponse,
+ createMockSpeaker,
+ getQueryClientWrapper,
+ SPEAKER_URLS,
+} from "../utils/testing/testUtils";
jest.mock("axios");
const mockedAxios = axios as jest.Mocked;
-const axiosHeaders = new AxiosHeaders();
-const payload: AxiosResponse = {
- status: 200,
- statusText: "OK",
- headers: {},
- config: {
- headers: axiosHeaders,
- },
- data: [
+// Create mock speakers
+const mockSpeaker1 = createMockSpeaker();
+const mockSpeaker2 = createMockSpeaker({
+ id: "2",
+ fullName: "Jane Doe",
+ profilePicture: "https://example.com/jane.jpg",
+ tagLine: "Data scientist",
+ bio: "I am a data scientist",
+ sessions: [],
+ links: [
{
- id: "1",
- fullName: "John Smith",
- profilePicture: "https://example.com/john.jpg",
- tagLine: "Software engineer",
- bio: "I am a software engineer",
- sessions: [
- {
- id: 4567,
- name: "sample session",
- },
- ],
- links: [
- {
- linkType: "Twitter",
- url: "https://twitter.com/johnsmith",
- title: "",
- },
- {
- linkType: "LinkedIn",
- url: "https://linkedin.com/in/johnsmith",
- title: "",
- },
- ],
+ linkType: "Twitter",
+ url: "https://twitter.com/janedoe",
+ title: "",
},
{
- id: "2",
- fullName: "Jane Doe",
- profilePicture: "https://example.com/jane.jpg",
- tagLine: "Data scientist",
- bio: "I am a data scientist",
- sessions: [],
- links: [
- {
- linkType: "Twitter",
- url: "https://twitter.com/janedoe",
- title: "",
- },
- {
- linkType: "LinkedIn",
- url: "https://linkedin.com/in/janedoe",
- title: "",
- },
- ],
+ linkType: "LinkedIn",
+ url: "https://linkedin.com/in/janedoe",
+ title: "",
},
],
-};
+});
+
+// Create mock response
+const payload = createMockAxiosResponse([mockSpeaker1, mockSpeaker2]);
describe("fetch speaker hook and speaker adapter", () => {
beforeAll(() => {
@@ -75,23 +48,15 @@ describe("fetch speaker hook and speaker adapter", () => {
});
it("should adapt from a server response with default URL", async () => {
- const queryClient = new QueryClient();
-
+ const { wrapper } = getQueryClientWrapper();
mockedAxios.get.mockImplementation(() => Promise.resolve(payload));
- const wrapper: FC>> = ({ children }) => {
- return (
-
- {children}
-
- );
- };
const { result } = renderHook(() => useFetchSpeakers(), {
wrapper,
});
await waitFor(() => result.current.isSuccess, {});
await waitFor(() => !result.current.isLoading, {});
- expect(mockedAxios.get).toHaveBeenCalledWith("https://sessionize.com/api/v2/xhudniix/view/Speakers");
+ expect(mockedAxios.get).toHaveBeenCalledWith(SPEAKER_URLS.DEFAULT);
expect(result.current.isLoading).toEqual(false);
expect(result.current.error).toEqual(null);
expect(result.current.data).toEqual(speakerAdapter(payload.data));
@@ -99,37 +64,9 @@ describe("fetch speaker hook and speaker adapter", () => {
it("should adapt from server response a query with id", async () => {
//Given
- const queryClient = new QueryClient();
+ const { wrapper } = getQueryClientWrapper();
mockedAxios.get.mockResolvedValueOnce(payload);
- const expectedPayload: IResponse[] = [
- {
- id: "1",
- bio: "I am a software engineer",
- fullName: "John Smith",
- links: [
- {
- linkType: "LinkedIn",
- url: "https://linkedin.com/in/johnsmith",
- title: "",
- },
- {
- url: "https://twitter.com/johnsmith",
- title: "",
- linkType: "Twitter",
- },
- ],
- profilePicture: "https://example.com/john.jpg",
- tagLine: "Software engineer",
- sessions: [{ id: 4567, name: "sample session" }],
- },
- ];
- const wrapper: FC>> = ({ children }) => {
- return (
-
- {children}
-
- );
- };
+ const expectedPayload: IResponse[] = [mockSpeaker1];
//When
const { result } = renderHook(() => useFetchSpeakers("1"), {
@@ -138,64 +75,43 @@ describe("fetch speaker hook and speaker adapter", () => {
await waitFor(() => result.current.isSuccess);
await waitFor(() => !result.current.isLoading, {});
//then
- expect(mockedAxios.get).toHaveBeenCalledWith("https://sessionize.com/api/v2/xhudniix/view/Speakers");
+ expect(mockedAxios.get).toHaveBeenCalledWith(SPEAKER_URLS.DEFAULT);
expect(result.current.data).toEqual(speakerAdapter(expectedPayload));
});
it("should use 2023 URL when '2023' is passed", async () => {
- const queryClient = new QueryClient();
+ const { wrapper } = getQueryClientWrapper();
mockedAxios.get.mockImplementation(() => Promise.resolve(payload));
- const wrapper: FC>> = ({ children }) => {
- return (
-
- {children}
-
- );
- };
const { result } = renderHook(() => useFetchSpeakers("2023"), {
wrapper,
});
await waitFor(() => result.current.isSuccess, {});
await waitFor(() => !result.current.isLoading, {});
- expect(mockedAxios.get).toHaveBeenCalledWith("https://sessionize.com/api/v2/ttsitynd/view/Speakers");
+ expect(mockedAxios.get).toHaveBeenCalledWith(SPEAKER_URLS["2023"]);
expect(result.current.isLoading).toEqual(false);
expect(result.current.error).toEqual(null);
expect(result.current.data).toEqual(speakerAdapter(payload.data));
});
it("should use 2024 URL when '2024' is passed", async () => {
- const queryClient = new QueryClient();
+ const { wrapper } = getQueryClientWrapper();
mockedAxios.get.mockImplementation(() => Promise.resolve(payload));
- const wrapper: FC>> = ({ children }) => {
- return (
-
- {children}
-
- );
- };
const { result } = renderHook(() => useFetchSpeakers("2024"), {
wrapper,
});
await waitFor(() => result.current.isSuccess, {});
await waitFor(() => !result.current.isLoading, {});
- expect(mockedAxios.get).toHaveBeenCalledWith("https://sessionize.com/api/v2/teq4asez/view/Speakers");
+ expect(mockedAxios.get).toHaveBeenCalledWith(SPEAKER_URLS["2024"]);
expect(result.current.isLoading).toEqual(false);
expect(result.current.error).toEqual(null);
expect(result.current.data).toEqual(speakerAdapter(payload.data));
});
it("should use custom URL when a URL is passed", async () => {
- const queryClient = new QueryClient();
+ const { wrapper } = getQueryClientWrapper();
mockedAxios.get.mockImplementation(() => Promise.resolve(payload));
- const wrapper: FC>> = ({ children }) => {
- return (
-
- {children}
-
- );
- };
const customUrl = "https://example.com/api/speakers";
const { result } = renderHook(() => useFetchSpeakers(customUrl), {
@@ -211,37 +127,9 @@ describe("fetch speaker hook and speaker adapter", () => {
it("should filter by ID when both a URL and ID are passed", async () => {
//Given
- const queryClient = new QueryClient();
+ const { wrapper } = getQueryClientWrapper();
mockedAxios.get.mockResolvedValueOnce(payload);
- const expectedPayload: IResponse[] = [
- {
- id: "1",
- bio: "I am a software engineer",
- fullName: "John Smith",
- links: [
- {
- linkType: "LinkedIn",
- url: "https://linkedin.com/in/johnsmith",
- title: "",
- },
- {
- url: "https://twitter.com/johnsmith",
- title: "",
- linkType: "Twitter",
- },
- ],
- profilePicture: "https://example.com/john.jpg",
- tagLine: "Software engineer",
- sessions: [{ id: 4567, name: "sample session" }],
- },
- ];
- const wrapper: FC>> = ({ children }) => {
- return (
-
- {children}
-
- );
- };
+ const expectedPayload: IResponse[] = [mockSpeaker1];
//When
const customUrl = "https://example.com/api/speakers";
@@ -254,4 +142,4 @@ describe("fetch speaker hook and speaker adapter", () => {
expect(mockedAxios.get).toHaveBeenCalledWith(customUrl);
expect(result.current.data).toEqual(speakerAdapter(expectedPayload));
});
-});
\ No newline at end of file
+});
diff --git a/src/hooks/useFetchTalks.test.tsx b/src/hooks/useFetchTalks.test.tsx
index d957fc35c..6907bd458 100644
--- a/src/hooks/useFetchTalks.test.tsx
+++ b/src/hooks/useFetchTalks.test.tsx
@@ -1,52 +1,36 @@
-import React, { FC } from "react";
-import { QueryClient, QueryClientProvider } from "react-query";
import { renderHook, waitFor } from "@testing-library/react";
-import axios, { AxiosHeaders, AxiosResponse } from "axios";
+import axios from "axios";
import {
useFetchLiveView,
useFetchTalks,
useFetchTalksById,
} from "./useFetchTalks";
-import { Liveview } from "../views/Talks/liveView.types";
-import { CategoryItemEnum, IGroup } from "../types/sessions";
+
+import { IGroup } from "../types/sessions";
+import {
+ createMockAxiosResponse,
+ createMockGroup,
+ createMockLiveview,
+ createMockSession,
+ getQueryClientWrapper,
+ SESSION_URLS,
+} from "../utils/testing/testUtils";
jest.mock("axios");
const mockedAxios = axios as jest.Mocked;
-const axiosHeaders = new AxiosHeaders();
-const queryClient = new QueryClient();
-const wrapper: FC>> = ({
- children,
-}) => (
- {children}
-);
describe("useFetchTalks", () => {
beforeEach(() => {
jest.clearAllMocks();
- queryClient.clear();
});
it("should use default URL when no parameter is provided", async () => {
- const mockData: IGroup[] = [
- {
- groupId: 1,
- groupName: "",
- sessions: [],
- isDefault: false,
- },
- ];
- const payload: AxiosResponse = {
- status: 200,
- statusText: "OK",
- headers: {},
- config: {
- headers: axiosHeaders,
- },
- data: mockData,
- };
+ const mockData: IGroup[] = [createMockGroup({ groupName: "" })];
+ const payload = createMockAxiosResponse(mockData);
mockedAxios.get.mockResolvedValue(payload);
+ const { wrapper } = getQueryClientWrapper();
const { result } = renderHook(() => useFetchTalks(), {
wrapper,
});
@@ -54,33 +38,17 @@ describe("useFetchTalks", () => {
await waitFor(() => result.current.isSuccess);
await waitFor(() => !result.current.isLoading);
- expect(mockedAxios.get).toHaveBeenCalledWith(
- "https://sessionize.com/api/v2/xhudniix/view/Sessions",
- );
+ expect(mockedAxios.get).toHaveBeenCalledWith(SESSION_URLS.DEFAULT);
expect(result.current.data).toEqual(mockData);
});
it("should use 2023 URL when '2023' is provided", async () => {
- const mockData: IGroup[] = [
- {
- groupId: 1,
- sessions: [],
- groupName: "test ",
- isDefault: false,
- },
- ];
- const payload: AxiosResponse = {
- status: 200,
- statusText: "OK",
- headers: {},
- config: {
- headers: axiosHeaders,
- },
- data: mockData,
- };
+ const mockData: IGroup[] = [createMockGroup({ groupName: "test " })];
+ const payload = createMockAxiosResponse(mockData);
mockedAxios.get.mockResolvedValue(payload);
+ const { wrapper } = getQueryClientWrapper();
const { result } = renderHook(() => useFetchTalks("2023"), {
wrapper,
});
@@ -88,33 +56,17 @@ describe("useFetchTalks", () => {
await waitFor(() => result.current.isSuccess);
await waitFor(() => !result.current.isLoading);
- expect(mockedAxios.get).toHaveBeenCalledWith(
- "https://sessionize.com/api/v2/ttsitynd/view/Sessions",
- );
+ expect(mockedAxios.get).toHaveBeenCalledWith(SESSION_URLS["2023"]);
expect(result.current.data).toEqual(mockData);
});
it("should use 2024 URL when '2024' is provided", async () => {
- const mockData: IGroup[] = [
- {
- groupId: 1,
- groupName: "test",
- isDefault: false,
- sessions: [],
- },
- ];
- const payload: AxiosResponse = {
- status: 200,
- statusText: "OK",
- headers: {},
- config: {
- headers: axiosHeaders,
- },
- data: mockData,
- };
+ const mockData: IGroup[] = [createMockGroup({ groupName: "test" })];
+ const payload = createMockAxiosResponse(mockData);
mockedAxios.get.mockResolvedValue(payload);
+ const { wrapper } = getQueryClientWrapper();
const { result } = renderHook(() => useFetchTalks("2024"), {
wrapper,
});
@@ -122,33 +74,17 @@ describe("useFetchTalks", () => {
await waitFor(() => result.current.isSuccess);
await waitFor(() => !result.current.isLoading);
- expect(mockedAxios.get).toHaveBeenCalledWith(
- "https://sessionize.com/api/v2/teq4asez/view/Sessions",
- );
+ expect(mockedAxios.get).toHaveBeenCalledWith(SESSION_URLS["2024"]);
expect(result.current.data).toEqual(mockData);
});
it("should use custom URL when a URL is provided", async () => {
- const mockData: IGroup[] = [
- {
- groupId: 1,
- groupName: "test",
- isDefault: false,
- sessions: [],
- },
- ];
- const payload: AxiosResponse = {
- status: 200,
- statusText: "OK",
- headers: {},
- config: {
- headers: axiosHeaders,
- },
- data: mockData,
- };
+ const mockData: IGroup[] = [createMockGroup({ groupName: "test" })];
+ const payload = createMockAxiosResponse(mockData);
mockedAxios.get.mockResolvedValue(payload);
+ const { wrapper } = getQueryClientWrapper();
const customUrl = "https://example.com/api/sessions";
const { result } = renderHook(() => useFetchTalks(customUrl), {
wrapper,
@@ -165,60 +101,20 @@ describe("useFetchTalks", () => {
describe("useFetchTalksById", () => {
beforeEach(() => {
jest.clearAllMocks();
- queryClient.clear();
});
it("should use default URL when no parameter is provided", async () => {
+ const mockSession = createMockSession();
const mockData: IGroup[] = [
- {
- groupId: 1,
- groupName: "test",
- isDefault: false,
- sessions: [
- {
- id: 123,
- title: "Test Session",
- description: "Test Description",
- endsAt: "2023-01-01T00:00:00Z",
- startsAt: "2023-01-01T00:00:00Z",
- track: "Test Track",
- categories: [
- {
- id: 1,
- name: CategoryItemEnum.Format,
- categoryItems: [{ id: 1, name: "test category" }],
- },
- ],
- speakers: [
- {
- id: "1",
- name: "Test Speaker",
- },
- ],
- questionAnswers: [
- {
- id: 1,
- question: "Test Question",
- answer: "Test Answer",
- questionType: "text",
- },
- ],
- },
- ],
- },
+ createMockGroup({
+ sessions: [mockSession],
+ }),
];
- const payload: AxiosResponse = {
- status: 200,
- statusText: "OK",
- headers: {},
- config: {
- headers: axiosHeaders,
- },
- data: mockData,
- };
+ const payload = createMockAxiosResponse(mockData);
mockedAxios.get.mockResolvedValue(payload);
+ const { wrapper } = getQueryClientWrapper();
const { result } = renderHook(() => useFetchTalksById("123"), {
wrapper,
});
@@ -226,64 +122,28 @@ describe("useFetchTalksById", () => {
await waitFor(() => result.current.isSuccess);
await waitFor(() => !result.current.isLoading);
- expect(mockedAxios.get).toHaveBeenCalledWith(
- "https://sessionize.com/api/v2/xhudniix/view/Sessions",
- );
+ expect(mockedAxios.get).toHaveBeenCalledWith(SESSION_URLS.DEFAULT);
const expectedData = mockData[0].sessions[0];
expect(result.current.data).toEqual(expectedData);
});
it("should use 2023 URL when '2023' is provided", async () => {
+ const mockSession = createMockSession({
+ track: "",
+ description: "",
+ startsAt: "2023-01-01T00:00:00",
+ });
const mockData: IGroup[] = [
- {
- groupId: 1,
+ createMockGroup({
groupName: "test ",
- isDefault: false,
- sessions: [
- {
- id: 123,
- title: "Test Session",
- track: "",
- description: "",
- endsAt: "2023-01-01T00:00:00Z",
- startsAt: "2023-01-01T00:00:00",
- categories: [
- {
- id: 1,
- name: CategoryItemEnum.Format,
- categoryItems: [{ id: 1, name: "test category" }],
- },
- ],
- speakers: [
- {
- id: "1",
- name: "Test Speaker",
- },
- ],
- questionAnswers: [
- {
- id: 1,
- question: "Test Question",
- answer: "Test Answer",
- questionType: "text",
- },
- ],
- },
- ],
- },
+ sessions: [mockSession],
+ }),
];
- const payload: AxiosResponse = {
- status: 200,
- statusText: "OK",
- headers: {},
- config: {
- headers: axiosHeaders,
- },
- data: mockData,
- };
+ const payload = createMockAxiosResponse(mockData);
mockedAxios.get.mockResolvedValue(payload);
+ const { wrapper } = getQueryClientWrapper();
const { result } = renderHook(() => useFetchTalksById("123", "2023"), {
wrapper,
});
@@ -291,9 +151,7 @@ describe("useFetchTalksById", () => {
await waitFor(() => result.current.isSuccess);
await waitFor(() => !result.current.isLoading);
- expect(mockedAxios.get).toHaveBeenCalledWith(
- "https://sessionize.com/api/v2/ttsitynd/view/Sessions",
- );
+ expect(mockedAxios.get).toHaveBeenCalledWith(SESSION_URLS["2023"]);
const expectedData = mockData[0].sessions[0];
expect(result.current.data).toEqual(expectedData);
});
@@ -302,28 +160,15 @@ describe("useFetchTalksById", () => {
describe("useFetchLiveView", () => {
beforeEach(() => {
jest.clearAllMocks();
- queryClient.clear();
});
it("should use default URL when no parameter is provided", async () => {
- const mockData: Liveview = {
- groupName: "",
- groupID: null,
- isDefault: false,
- sessions: [],
- };
- const payload: AxiosResponse = {
- status: 200,
- statusText: "OK",
- headers: {},
- config: {
- headers: axiosHeaders,
- },
- data: [mockData],
- };
+ const mockData = createMockLiveview();
+ const payload = createMockAxiosResponse([mockData]);
mockedAxios.get.mockResolvedValue(payload);
+ const { wrapper } = getQueryClientWrapper();
const { result } = renderHook(() => useFetchLiveView(), {
wrapper,
});
@@ -331,31 +176,17 @@ describe("useFetchLiveView", () => {
await waitFor(() => result.current.isSuccess);
await waitFor(() => !result.current.isLoading);
- expect(mockedAxios.get).toHaveBeenCalledWith(
- "https://sessionize.com/api/v2/xhudniix/view/Sessions",
- );
+ expect(mockedAxios.get).toHaveBeenCalledWith(SESSION_URLS.DEFAULT);
expect(result.current.data).toEqual(payload.data[0]);
});
it("should use 2024 URL when '2024' is provided", async () => {
- const mockData: Liveview = {
- groupID: null,
- groupName: "",
- isDefault: false,
- sessions: [],
- };
- const payload: AxiosResponse = {
- status: 200,
- statusText: "OK",
- headers: {},
- config: {
- headers: axiosHeaders,
- },
- data: [mockData],
- };
+ const mockData = createMockLiveview();
+ const payload = createMockAxiosResponse([mockData]);
mockedAxios.get.mockResolvedValue(payload);
+ const { wrapper } = getQueryClientWrapper();
const { result } = renderHook(() => useFetchLiveView("2024"), {
wrapper,
});
@@ -363,9 +194,7 @@ describe("useFetchLiveView", () => {
await waitFor(() => result.current.isSuccess);
await waitFor(() => !result.current.isLoading);
- expect(mockedAxios.get).toHaveBeenCalledWith(
- "https://sessionize.com/api/v2/teq4asez/view/Sessions",
- );
+ expect(mockedAxios.get).toHaveBeenCalledWith(SESSION_URLS["2024"]);
expect(result.current.data).toEqual(payload.data[0]);
});
});
diff --git a/src/utils/testing/testUtils.tsx b/src/utils/testing/testUtils.tsx
new file mode 100644
index 000000000..5733f01e0
--- /dev/null
+++ b/src/utils/testing/testUtils.tsx
@@ -0,0 +1,147 @@
+import React, { FC } from "react";
+import { QueryClient, QueryClientProvider } from "react-query";
+import { render, RenderOptions, RenderResult } from "@testing-library/react";
+import { AxiosHeaders, AxiosResponse } from "axios";
+
+// Re-export everything from testing-library
+export * from "@testing-library/react";
+
+// Create a custom render function that includes the QueryClientProvider
+export function renderWithQueryClient(
+ ui: React.ReactElement,
+ options?: Omit,
+): RenderResult {
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ },
+ },
+ });
+
+ const wrapper: FC> = ({ children }) => (
+ {children}
+ );
+
+ return render(ui, { wrapper, ...options });
+}
+
+// Create a function to get a QueryClient and wrapper for use with renderHook
+export function getQueryClientWrapper() {
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ },
+ },
+ });
+
+ const wrapper: FC> = ({ children }) => (
+ {children}
+ );
+
+ return { queryClient, wrapper };
+}
+
+// Create a function to create a mock axios response
+export function createMockAxiosResponse(data: T): AxiosResponse {
+ const axiosHeaders = new AxiosHeaders();
+ return {
+ status: 200,
+ statusText: "OK",
+ headers: {},
+ config: {
+ headers: axiosHeaders,
+ },
+ data,
+ };
+}
+
+// Session URLs
+export const SESSION_URLS = {
+ DEFAULT: "https://sessionize.com/api/v2/xhudniix/view/Sessions",
+ "2023": "https://sessionize.com/api/v2/ttsitynd/view/Sessions",
+ "2024": "https://sessionize.com/api/v2/teq4asez/view/Sessions",
+};
+
+// Speaker URLs
+export const SPEAKER_URLS = {
+ DEFAULT: "https://sessionize.com/api/v2/xhudniix/view/Speakers",
+ "2023": "https://sessionize.com/api/v2/ttsitynd/view/Speakers",
+ "2024": "https://sessionize.com/api/v2/teq4asez/view/Speakers",
+};
+
+// Mock data factories
+export const createMockSpeaker = (overrides = {}) => ({
+ id: "1",
+ fullName: "John Smith",
+ profilePicture: "https://example.com/john.jpg",
+ tagLine: "Software engineer",
+ bio: "I am a software engineer",
+ sessions: [
+ {
+ id: 4567,
+ name: "sample session",
+ },
+ ],
+ links: [
+ {
+ linkType: "Twitter",
+ url: "https://twitter.com/johnsmith",
+ title: "",
+ },
+ {
+ linkType: "LinkedIn",
+ url: "https://linkedin.com/in/johnsmith",
+ title: "",
+ },
+ ],
+ ...overrides,
+});
+
+export const createMockSession = (overrides = {}) => ({
+ id: 123,
+ title: "Test Session",
+ description: "Test Description",
+ endsAt: "2023-01-01T00:00:00Z",
+ startsAt: "2023-01-01T00:00:00Z",
+ track: "Test Track",
+ categories: [
+ {
+ id: 1,
+ name: "Format",
+ categoryItems: [{ id: 1, name: "test category" }],
+ },
+ ],
+ speakers: [
+ {
+ id: "1",
+ name: "Test Speaker",
+ },
+ ],
+ questionAnswers: [
+ {
+ id: 1,
+ question: "Test Question",
+ answer: "Test Answer",
+ questionType: "text",
+ },
+ ],
+ ...overrides,
+});
+
+export const createMockGroup = (overrides = {}) => ({
+ groupId: 1,
+ groupName: "Test Group",
+ isDefault: false,
+ sessions: [],
+ ...overrides,
+});
+
+export const createMockLiveview = (overrides = {}) => ({
+ groupID: null,
+ groupName: "",
+ isDefault: false,
+ sessions: [],
+ ...overrides,
+});
diff --git a/src/views/Talks/LiveView.test.tsx b/src/views/Talks/LiveView.test.tsx
index cc02d42b4..556cb3fca 100644
--- a/src/views/Talks/LiveView.test.tsx
+++ b/src/views/Talks/LiveView.test.tsx
@@ -1,16 +1,10 @@
import LiveView from "./LiveView";
-import { QueryClient, QueryClientProvider } from "react-query";
-import { render, screen } from "@testing-library/react";
import React from "react";
+import { renderWithQueryClient, screen } from "../../utils/testing/testUtils";
describe("Live view component", () => {
it("renders without crashing", () => {
- const queryClient = new QueryClient();
- render(
-
-
- ,
- );
+ renderWithQueryClient();
const titleElement = screen.getByText(/Live Schedule/);
expect(titleElement).toBeInTheDocument();
});
diff --git a/src/views/Talks/Talks.test.tsx b/src/views/Talks/Talks.test.tsx
index 6e405ef18..216875fff 100644
--- a/src/views/Talks/Talks.test.tsx
+++ b/src/views/Talks/Talks.test.tsx
@@ -1,36 +1,21 @@
import React from "react";
-import { render, screen } from "@testing-library/react";
+import { screen } from "@testing-library/react";
import Talks from "./Talks";
-import { QueryClient, QueryClientProvider } from "react-query";
+import { renderWithQueryClient } from "../../utils/testing/testUtils";
describe("Talks", () => {
it("renders without errors", () => {
- const queryClient = new QueryClient();
- render(
-
-
-
- );
+ renderWithQueryClient();
});
it("renders the correct title", () => {
- const queryClient = new QueryClient();
- render(
-
-
-
- );
+ renderWithQueryClient();
const titleElement = screen.getByText(/TALKS/);
expect(titleElement).toBeInTheDocument();
});
it("renders the correct subtitle", () => {
- const queryClient = new QueryClient();
- render(
-
-
-
- );
+ renderWithQueryClient();
const subtitleElement = screen.getByText(
/speakers coming from all corners of the world/i
);
@@ -38,33 +23,18 @@ describe("Talks", () => {
});
it("renders a filter by track dropdown", () => {
- const queryClient = new QueryClient();
- render(
-
-
-
- );
+ renderWithQueryClient();
const dropdownElement = screen.getByText("Loading");
expect(dropdownElement).toBeInTheDocument();
});
it("renders a loading message when talks are being fetched", () => {
- const queryClient = new QueryClient();
- render(
-
-
-
- );
+ renderWithQueryClient();
expect(screen.getByText("Loading")).toBeInTheDocument();
});
it("renders a message when no talks are selected", () => {
- const queryClient = new QueryClient();
- render(
-
-
-
- );
+ renderWithQueryClient();
const dropdownElement = screen.getByText("Loading");
expect(dropdownElement).toBeInTheDocument();
});
From ea04f17209474ccbc2a5233be7b388261ba6bf6e Mon Sep 17 00:00:00 2001
From: Anyul Rivas
Date: Tue, 25 Mar 2025 09:56:07 +0100
Subject: [PATCH 09/11] chore: spell check
---
wordlist.txt | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/wordlist.txt b/wordlist.txt
index fe1378549..5c8efab74 100644
--- a/wordlist.txt
+++ b/wordlist.txt
@@ -1,21 +1,27 @@
+Axios
BvU
CFP
Cfp
-dataTransformer
DOM
DevBCN
DevBcn
ESLint
Facebook
Farga
+Frontend
KCD
LkR
PLo
+PrimeReact
StyledSelectTrack
+Swiper
Twitter
+TypeScript
+UI
Veepee
amycuddyTED
analytics
+dataTransformer
devbcn
dom
etDpvu
@@ -59,8 +65,10 @@ svg
th
toHaveTextContent
twimg
-urlOrYear
urlOrId
+urlOrYear
+useFetchSpeakers
+useFetchTalks
veepee
vilojona
webfonts
From 6e8fa8eba5a765cab4cae2356495d29e12bbe5ac Mon Sep 17 00:00:00 2001
From: Anyul Rivas
Date: Tue, 25 Mar 2025 12:27:52 +0100
Subject: [PATCH 10/11] refactor: remove duplication on track information and
talk card
---
.../TalkDetail/TalkDetailContainer2023.tsx | 12 +-
src/2023/Talks/Talks2023.tsx | 8 +-
...ner.tsx => MeetingDetailContainer2024.tsx} | 16 +--
src/2024/Talks/Talks2024.tsx | 8 +-
src/App.tsx | 116 ++++++++--------
src/components/Talk/TalkCard.tsx | 103 +-------------
src/components/Talk/TrackInformation.tsx | 41 ------
src/components/common/TalkCard.tsx | 127 ++++++++++++++++++
.../common}/TrackInformation.tsx | 18 ++-
src/hooks/useFetchTalks.ts | 2 +-
...tainer2024.tsx => TalkDetailContainer.tsx} | 16 +--
src/views/Talks/TalkCardAdapter.test.ts | 52 ++++++-
src/views/Talks/TalkCardAdapter.ts | 3 +-
src/views/Talks/Talks.tsx | 8 +-
src/views/Talks/components/TalkCard.tsx | 100 +-------------
15 files changed, 298 insertions(+), 332 deletions(-)
rename src/2024/TalkDetail/{MeetingDetailContainer.tsx => MeetingDetailContainer2024.tsx} (83%)
delete mode 100644 src/components/Talk/TrackInformation.tsx
create mode 100644 src/components/common/TalkCard.tsx
rename src/{views/Talks/components => components/common}/TrackInformation.tsx (68%)
rename src/views/MeetingDetail/{TalkDetailContainer2024.tsx => TalkDetailContainer.tsx} (83%)
diff --git a/src/2023/TalkDetail/TalkDetailContainer2023.tsx b/src/2023/TalkDetail/TalkDetailContainer2023.tsx
index 23a6f2259..5d3bef4b1 100644
--- a/src/2023/TalkDetail/TalkDetailContainer2023.tsx
+++ b/src/2023/TalkDetail/TalkDetailContainer2023.tsx
@@ -21,10 +21,8 @@ const TalkDetailContainer2023: FC> = () => {
const { isLoading, error, data } = useFetchTalksById(id!, "2023");
const { data: speakerData } = useFetchSpeakers("2023");
- const getTalkSpeakers = (
- data: Session[] | undefined,
- ): string[] | undefined => {
- const speakers = data?.[0]?.speakers;
+ const getTalkSpeakers = (data: Session | undefined): string[] | undefined => {
+ const speakers = data?.speakers;
return speakers?.map((speaker) => speaker.id);
};
@@ -33,12 +31,10 @@ const TalkDetailContainer2023: FC> = () => {
(speaker) => talkSpeakers?.includes(speaker.id),
);
- const adaptedMeeting = sessionAdapter(data?.at(0));
+ const adaptedMeeting = sessionAdapter(data);
useEffect(() => {
- document.title = `${data?.at(0)?.title} - DevBcn - ${
- conferenceData.edition
- }`;
+ document.title = `${data?.title} - DevBcn - ${conferenceData.edition}`;
}, [data]);
if (error) {
diff --git a/src/2023/Talks/Talks2023.tsx b/src/2023/Talks/Talks2023.tsx
index 90468590f..fcec96679 100644
--- a/src/2023/Talks/Talks2023.tsx
+++ b/src/2023/Talks/Talks2023.tsx
@@ -18,7 +18,7 @@ import { Dropdown, DropdownChangeEvent } from "primereact/dropdown";
import "primereact/resources/primereact.min.css";
import "primereact/resources/themes/lara-light-indigo/theme.css";
import "../../styles/theme.css";
-import TrackInformation from "../../components/Talk/TrackInformation";
+import TrackInformation from "../../components/common/TrackInformation";
interface TrackInfo {
name: string;
@@ -126,7 +126,11 @@ const Talks2023: FC> = () => {
/>
{filteredTalks.map((track) => (
-
+
))}
>
)}
diff --git a/src/2024/TalkDetail/MeetingDetailContainer.tsx b/src/2024/TalkDetail/MeetingDetailContainer2024.tsx
similarity index 83%
rename from src/2024/TalkDetail/MeetingDetailContainer.tsx
rename to src/2024/TalkDetail/MeetingDetailContainer2024.tsx
index 3c598d16b..006795211 100644
--- a/src/2024/TalkDetail/MeetingDetailContainer.tsx
+++ b/src/2024/TalkDetail/MeetingDetailContainer2024.tsx
@@ -17,15 +17,13 @@ import { sessionAdapter } from "../../services/sessionsAdapter";
const StyledContainer = styled.div`
background-color: ${Color.WHITE};
`;
-const MeetingDetailContainer: FC> = () => {
+const MeetingDetailContainer2024: FC> = () => {
const { id } = useParams<{ id: string }>();
const { isLoading, error, data } = useFetchTalksById(id!, "2024");
const { data: speakerData } = useFetchSpeakers("2024");
- const getTalkSpeakers = (
- data: Session[] | undefined,
- ): string[] | undefined => {
- const speakers = data?.[0]?.speakers;
+ const getTalkSpeakers = (data: Session | undefined): string[] | undefined => {
+ const speakers = data?.speakers;
return speakers?.map((speaker) => speaker.id);
};
@@ -34,12 +32,10 @@ const MeetingDetailContainer: FC> = () => {
(speaker) => talkSpeakers?.includes(speaker.id),
);
- const adaptedMeeting = sessionAdapter(data?.at(0));
+ const adaptedMeeting = sessionAdapter(data);
useEffect(() => {
- document.title = `${data?.at(0)?.title} - DevBcn - ${
- conferenceData.edition
- }`;
+ document.title = `${data?.title} - DevBcn - ${conferenceData.edition}`;
}, [data]);
if (error) {
@@ -68,4 +64,4 @@ const MeetingDetailContainer: FC> = () => {
);
};
-export default MeetingDetailContainer;
+export default MeetingDetailContainer2024;
diff --git a/src/2024/Talks/Talks2024.tsx b/src/2024/Talks/Talks2024.tsx
index 6375fa541..ed056fd31 100644
--- a/src/2024/Talks/Talks2024.tsx
+++ b/src/2024/Talks/Talks2024.tsx
@@ -19,7 +19,7 @@ import {
StyledTitleIcon,
StyledWaveContainer,
} from "../../views/Talks/Talks.style";
-import TrackInformation from "../../components/Talk/TrackInformation";
+import TrackInformation from "../../components/common/TrackInformation";
interface TrackInfo {
name: string;
@@ -128,7 +128,11 @@ const Talks2024: FC> = () => {
/>
{filteredTalks.map((track) => (
-
+
))}
>
)
diff --git a/src/App.tsx b/src/App.tsx
index 9c64aa744..5bd60cb4e 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,51 +1,52 @@
import { Link, Route, Routes } from "react-router";
import {
- ROUTE_2023_ATTENDEE,
- ROUTE_2023_CFP,
- ROUTE_2023_COMMUNITIES,
- ROUTE_2023_DIVERSITY,
- ROUTE_2023_HOME,
- ROUTE_2023_JOB_OFFERS,
- ROUTE_2023_SCHEDULE,
- ROUTE_2023_SESSION_FEEDBACK,
- ROUTE_2023_SPEAKER_DETAIL_PLAIN,
- ROUTE_2023_SPEAKER_INFO,
- ROUTE_2023_SPEAKERS,
- ROUTE_2023_TALK_DETAIL_PLAIN,
- ROUTE_2023_TALKS,
- ROUTE_2023_WORKSHOPS,
- ROUTE_2024_ATTENDEE,
- ROUTE_2024_CFP,
- ROUTE_2024_COMMUNITIES,
- ROUTE_2024_DIVERSITY,
- ROUTE_2024_HOME,
- ROUTE_2024_JOB_OFFERS,
- ROUTE_2024_SCHEDULE,
- ROUTE_2024_SESSION_FEEDBACK,
- ROUTE_2024_SPEAKER_DETAIL_PLAIN,
- ROUTE_2024_SPEAKER_INFO,
- ROUTE_2024_SPEAKERS,
- ROUTE_2024_TALK_DETAIL_PLAIN,
- ROUTE_2024_TALKS,
- ROUTE_2024_WORKSHOPS,
- ROUTE_ABOUT_US,
- ROUTE_ACCOMMODATION,
- ROUTE_CFP,
- ROUTE_CODE_OF_CONDUCT,
- ROUTE_CONDITIONS,
- ROUTE_COOKIES,
- ROUTE_DIVERSITY,
- ROUTE_HOME,
- ROUTE_JOB_OFFERS,
- ROUTE_KCD,
- ROUTE_MEETING_DETAIL_PLAIN,
- ROUTE_SCHEDULE,
- ROUTE_SPEAKER_DETAIL_PLAIN,
- ROUTE_SPEAKER_INFO,
- ROUTE_SPEAKERS,
- ROUTE_SPONSORSHIP,
- ROUTE_TALKS,
- ROUTE_TRAVEL, ROUTE_WORKSHOPS,
+ ROUTE_2023_ATTENDEE,
+ ROUTE_2023_CFP,
+ ROUTE_2023_COMMUNITIES,
+ ROUTE_2023_DIVERSITY,
+ ROUTE_2023_HOME,
+ ROUTE_2023_JOB_OFFERS,
+ ROUTE_2023_SCHEDULE,
+ ROUTE_2023_SESSION_FEEDBACK,
+ ROUTE_2023_SPEAKER_DETAIL_PLAIN,
+ ROUTE_2023_SPEAKER_INFO,
+ ROUTE_2023_SPEAKERS,
+ ROUTE_2023_TALK_DETAIL_PLAIN,
+ ROUTE_2023_TALKS,
+ ROUTE_2023_WORKSHOPS,
+ ROUTE_2024_ATTENDEE,
+ ROUTE_2024_CFP,
+ ROUTE_2024_COMMUNITIES,
+ ROUTE_2024_DIVERSITY,
+ ROUTE_2024_HOME,
+ ROUTE_2024_JOB_OFFERS,
+ ROUTE_2024_SCHEDULE,
+ ROUTE_2024_SESSION_FEEDBACK,
+ ROUTE_2024_SPEAKER_DETAIL_PLAIN,
+ ROUTE_2024_SPEAKER_INFO,
+ ROUTE_2024_SPEAKERS,
+ ROUTE_2024_TALK_DETAIL_PLAIN,
+ ROUTE_2024_TALKS,
+ ROUTE_2024_WORKSHOPS,
+ ROUTE_ABOUT_US,
+ ROUTE_ACCOMMODATION,
+ ROUTE_CFP,
+ ROUTE_CODE_OF_CONDUCT,
+ ROUTE_CONDITIONS,
+ ROUTE_COOKIES,
+ ROUTE_DIVERSITY,
+ ROUTE_HOME,
+ ROUTE_JOB_OFFERS,
+ ROUTE_KCD,
+ ROUTE_MEETING_DETAIL_PLAIN,
+ ROUTE_SCHEDULE,
+ ROUTE_SPEAKER_DETAIL_PLAIN,
+ ROUTE_SPEAKER_INFO,
+ ROUTE_SPEAKERS,
+ ROUTE_SPONSORSHIP,
+ ROUTE_TALKS,
+ ROUTE_TRAVEL,
+ ROUTE_WORKSHOPS,
} from "./constants/routes";
import Footer from "./components/Footer/Footer";
@@ -93,12 +94,13 @@ import JobOffers from "./views/JobOffers/JobOffers";
import { HomeWrapper2024 } from "./2024/HomeWrapper2024";
import Speakers2024 from "./2024/Speakers/Speakers2024";
import Talks2024 from "./2024/Talks/Talks2024";
-import TalkDetailContainer2024 from "./views/MeetingDetail/TalkDetailContainer2024";
+import TalkDetailContainer from "./views/MeetingDetail/TalkDetailContainer";
import SpeakerDetailContainer2024 from "./2024/SpeakerDetail/SpeakerDetailContainer2024";
import CfpSection2024 from "./2024/Cfp/CfpSection2024";
import Workshops from "./views/Workshops/Workshops";
import Schedule2024 from "./2024/Schedule/Schedule2024";
import JobOffers2024 from "./2024/JobOffers/JobOffers2024";
+import MeetingDetailContainer2024 from "./2024/TalkDetail/MeetingDetailContainer2024";
const StyledAppWrapper = styled.div`
position: relative;
@@ -149,14 +151,14 @@ const App: FC> = () => {
}
/>
- }>
-
-
- }
- />
+ }>
+
+
+ }
+ />
{/*}>
} />*/}
@@ -279,7 +281,7 @@ const App: FC> = () => {
path={ROUTE_MEETING_DETAIL_PLAIN}
element={
}>
-
+
}
/>
@@ -432,7 +434,7 @@ const App: FC> = () => {
path={ROUTE_2024_TALK_DETAIL_PLAIN}
element={
}>
-
+
}
/>
diff --git a/src/components/Talk/TalkCard.tsx b/src/components/Talk/TalkCard.tsx
index 1daf9dcb3..9c4f92d8f 100644
--- a/src/components/Talk/TalkCard.tsx
+++ b/src/components/Talk/TalkCard.tsx
@@ -1,99 +1,10 @@
-import React, {FC} from "react";
-import {Link} from "react-router";
-import {Tag} from "../Tag/Tag";
-import {
- ROUTE_2024_SPEAKER_DETAIL,
- ROUTE_2024_TALK_DETAIL,
-} from "../../constants/routes";
-import {Color} from "../../styles/colors";
-import {StyledJobsInfo} from "../JobOffers/JobsCard";
-import {
- StyledSessionCard,
- StyledSessionText,
- StyledTagsWrapper,
- StyledTalkSpeaker,
- StyledTalkTitle
-} from "../../views/Talks/Talks.style";
-import {StyledVoteTalkLink} from "../../views/MeetingDetail/MeetingDetail";
-import {
- extractSessionCategoryInfo,
- extractSessionTags
-} from "../../services/sessionsAdapter";
-import {
- CategoryItemEnum,
- QuestionAnswers,
- SessionCategory,
- SessionSpeaker
-} from "../../types/sessions";
+import React from "react";
+import CommonTalkCard, { TalkCardProps } from "../common/TalkCard";
-export interface TalkCardProps {
- talk: {
- id: number;
- title: string;
- talkImage?: number;
- speakers: SessionSpeaker[];
- level?: string;
- link?: string;
- tags?: string[];
- track: string;
- categories: SessionCategory[];
- questionAnswers: QuestionAnswers[];
- };
- showTrack?: boolean;
-}
+export type { TalkCardProps };
-export const TalkCard: FC> = ({
- showTrack = false,
- talk,
- }) => {
- return (
-
-
-
- {talk.title}
-
-
- {talk.speakers.map((speaker: SessionSpeaker) => (
-
-
- {speaker.name}
-
-
- ))}
-
-
- {`${extractSessionCategoryInfo(
- talk.categories,
- CategoryItemEnum.Format,
- )} `}
- {extractSessionCategoryInfo(talk.categories)}{" "}
-
- {showTrack && (
-
- Track:
- {extractSessionCategoryInfo(
- talk.categories,
- CategoryItemEnum.Track,
- )}
-
- )}
-
- {extractSessionTags(talk.questionAnswers)?.map((tag) => {
- return ;
- })}
-
-
-
- 🗳️ Vote this talk
-
-
-
-
- );
+export const TalkCard: React.FC> = (
+ props,
+) => {
+ return ;
};
diff --git a/src/components/Talk/TrackInformation.tsx b/src/components/Talk/TrackInformation.tsx
deleted file mode 100644
index f7ede5333..000000000
--- a/src/components/Talk/TrackInformation.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import React, {FC, useMemo} from "react";
-import {TalkCard} from "./TalkCard";
-import {
- StyledSessionSection,
- StyledTrackInfo
-} from "../../views/Talks/Talks.style";
-
-import {IGroup} from "../../types/sessions";
-
-interface TrackInfoProps {
- track: IGroup;
-}
-
-const useGenerateAnchorName = (trackName: string) => {
- const visibleTodos = useMemo(() => {
- return trackName
- .split(/\s+/)
- .map((word) => word.replace(/,$/, "").toLowerCase());
- }, [trackName]);
- return visibleTodos[0];
-};
-
-const TrackInformation: FC> = ({
- track,
- }) => {
- const anchorName = useGenerateAnchorName(track.groupName);
-
- return (
-
- {track.groupName}
-
- {Array.isArray(track.sessions) &&
- track.sessions.map((session) => (
-
- ))}
-
-
- );
-};
-
-export default React.memo(TrackInformation);
diff --git a/src/components/common/TalkCard.tsx b/src/components/common/TalkCard.tsx
new file mode 100644
index 000000000..1462cab7b
--- /dev/null
+++ b/src/components/common/TalkCard.tsx
@@ -0,0 +1,127 @@
+import React, { FC } from "react";
+import { Link } from "react-router";
+import { Tag } from "../Tag/Tag";
+import {
+ ROUTE_2023_SPEAKER_DETAIL,
+ ROUTE_2023_TALK_DETAIL,
+ ROUTE_2024_SPEAKER_DETAIL,
+ ROUTE_2024_TALK_DETAIL,
+ ROUTE_SPEAKER_DETAIL,
+ ROUTE_TALK_DETAIL,
+} from "../../constants/routes";
+import { Color } from "../../styles/colors";
+import { StyledJobsInfo } from "../JobOffers/JobsCard";
+import {
+ StyledSessionCard,
+ StyledSessionText,
+ StyledTagsWrapper,
+ StyledTalkSpeaker,
+ StyledTalkTitle,
+} from "../../views/Talks/Talks.style";
+import { StyledVoteTalkLink } from "../../views/MeetingDetail/MeetingDetail";
+import {
+ extractSessionCategoryInfo,
+ extractSessionTags,
+} from "../../services/sessionsAdapter";
+import {
+ CategoryItemEnum,
+ QuestionAnswers,
+ SessionCategory,
+ SessionSpeaker,
+} from "../../types/sessions";
+
+export interface TalkCardProps {
+ talk: {
+ id: number;
+ title: string;
+ talkImage?: number;
+ speakers: SessionSpeaker[];
+ level?: string;
+ link?: string;
+ tags?: string[];
+ track: string;
+ categories: SessionCategory[];
+ questionAnswers: QuestionAnswers[];
+ };
+ year: string;
+ showTrack?: boolean;
+}
+
+const getTalkDetailRoute = (year: string): string => {
+ if (year === "2024") {
+ return ROUTE_2024_TALK_DETAIL;
+ }
+ if (year === "2023") {
+ return ROUTE_2023_TALK_DETAIL;
+ }
+
+ return ROUTE_TALK_DETAIL;
+};
+
+const getSpeakerDetailRoute = (year: string): string => {
+ if (year === "2023") {
+ return ROUTE_2023_SPEAKER_DETAIL;
+ }
+ if (year === "2024") {
+ return ROUTE_2024_SPEAKER_DETAIL;
+ }
+
+ return ROUTE_SPEAKER_DETAIL;
+};
+
+export const TalkCard: FC> = ({
+ showTrack = false,
+ talk,
+ year,
+}) => {
+ return (
+
+
+
+ {talk.title}
+
+
+ {talk.speakers.map((speaker: SessionSpeaker) => (
+
+
+ {speaker.name}
+
+
+ ))}
+
+
+ {`${extractSessionCategoryInfo(
+ talk.categories,
+ CategoryItemEnum.Format,
+ )} `}
+ {extractSessionCategoryInfo(talk.categories)}{" "}
+
+ {showTrack && (
+
+ Track:
+ {extractSessionCategoryInfo(
+ talk.categories,
+ CategoryItemEnum.Track,
+ )}
+
+ )}
+
+ {extractSessionTags(talk.questionAnswers)?.map((tag) => {
+ return ;
+ })}
+
+
+
+ 🗳️ Vote this talk
+
+
+
+
+ );
+};
+
+export default TalkCard;
diff --git a/src/views/Talks/components/TrackInformation.tsx b/src/components/common/TrackInformation.tsx
similarity index 68%
rename from src/views/Talks/components/TrackInformation.tsx
rename to src/components/common/TrackInformation.tsx
index ee71c5dc5..d5fa2a7b0 100644
--- a/src/views/Talks/components/TrackInformation.tsx
+++ b/src/components/common/TrackInformation.tsx
@@ -1,10 +1,15 @@
-import React, {FC, useMemo} from "react";
-import {TalkCard} from "./TalkCard";
-import {StyledSessionSection, StyledTrackInfo} from "../Talks.style";
-import {IGroup} from "../../../types/sessions";
+import React, { FC, useMemo } from "react";
+import TalkCard from "./TalkCard";
+import {
+ StyledSessionSection,
+ StyledTrackInfo,
+} from "../../views/Talks/Talks.style";
+
+import { IGroup } from "../../types/sessions";
interface TrackInfoProps {
track: IGroup;
+ year: string;
}
const useGenerateAnchorName = (trackName: string) => {
@@ -18,6 +23,7 @@ const useGenerateAnchorName = (trackName: string) => {
const TrackInformation: FC> = ({
track,
+ year,
}) => {
const anchorName = useGenerateAnchorName(track.groupName);
@@ -27,11 +33,11 @@ const TrackInformation: FC> = ({
{Array.isArray(track.sessions) &&
track.sessions.map((session) => (
-
+
))}
);
};
-export default React.memo(TrackInformation);
+export default React.memo(TrackInformation);
\ No newline at end of file
diff --git a/src/hooks/useFetchTalks.ts b/src/hooks/useFetchTalks.ts
index b25df6d9c..2dac2c425 100644
--- a/src/hooks/useFetchTalks.ts
+++ b/src/hooks/useFetchTalks.ts
@@ -59,7 +59,7 @@ export const useFetchTalksById = (
id: string,
urlOrYear?: string,
): UseQueryResult => {
- return useFetchTalksBase("talks", urlOrYear, (data) => {
+ return useFetchTalksBase("talks", urlOrYear, (data: any[]) => {
const sessions = data
.map((track: IGroup) => track.sessions)
.flat(1)
diff --git a/src/views/MeetingDetail/TalkDetailContainer2024.tsx b/src/views/MeetingDetail/TalkDetailContainer.tsx
similarity index 83%
rename from src/views/MeetingDetail/TalkDetailContainer2024.tsx
rename to src/views/MeetingDetail/TalkDetailContainer.tsx
index 63e347d05..794a68c93 100644
--- a/src/views/MeetingDetail/TalkDetailContainer2024.tsx
+++ b/src/views/MeetingDetail/TalkDetailContainer.tsx
@@ -16,15 +16,13 @@ import { Session } from "../../types/sessions";
const StyledContainer = styled.div`
background-color: ${Color.WHITE};
`;
-const TalkDetailContainer2024: FC> = () => {
+const TalkDetailContainer: FC> = () => {
const { id } = useParams<{ id: string }>();
const { isLoading, error, data } = useFetchTalksById(id!);
const { data: speakerData } = useFetchSpeakers();
- const getTalkSpeakers = (
- data: Session[] | undefined,
- ): string[] | undefined => {
- const speakers = data?.[0]?.speakers;
+ const getTalkSpeakers = (data: Session | undefined): string[] | undefined => {
+ const speakers = data?.speakers;
return speakers?.map((speaker) => speaker.id);
};
@@ -33,12 +31,10 @@ const TalkDetailContainer2024: FC> = () => {
(speaker) => talkSpeakers?.includes(speaker.id),
);
- const adaptedMeeting = sessionAdapter(data?.at(0));
+ const adaptedMeeting = sessionAdapter(data);
useEffect(() => {
- document.title = `${data?.at(0)?.title} - DevBcn - ${
- conferenceData.edition
- }`;
+ document.title = `${data?.title} - DevBcn - ${conferenceData.edition}`;
}, [data]);
if (error) {
@@ -67,4 +63,4 @@ const TalkDetailContainer2024: FC> = () => {
);
};
-export default TalkDetailContainer2024;
+export default TalkDetailContainer;
diff --git a/src/views/Talks/TalkCardAdapter.test.ts b/src/views/Talks/TalkCardAdapter.test.ts
index c4b0c907b..d5b7934a9 100644
--- a/src/views/Talks/TalkCardAdapter.test.ts
+++ b/src/views/Talks/TalkCardAdapter.test.ts
@@ -3,7 +3,7 @@ import { UngroupedSession } from "./liveView.types";
import { faker } from "@faker-js/faker";
describe("talkCardAdapter", () => {
- it("should return the correct TalkCardProps object", () => {
+ it("should return the correct TalkCardProps object with default year", () => {
const session: UngroupedSession = {
id: "1",
title: faker.lorem.words(5),
@@ -49,5 +49,55 @@ describe("talkCardAdapter", () => {
expect(result.talk.title).toBe(session.title);
expect(result.talk.speakers.at(0)?.name).toBe(session.speakers[0].name);
+ expect(result.year).toBe("2024"); // Default year
+ });
+
+ it("should use the provided year", () => {
+ const session: UngroupedSession = {
+ id: "1",
+ title: faker.lorem.words(5),
+ speakers: [{ id: "1", name: faker.person.fullName() }],
+ room: "Frontend Track",
+ startsAt: faker.date.past().toLocaleString(),
+ endsAt: faker.date.future().toLocaleString(),
+ description: faker.lorem.lines(1),
+ isServiceSession: false,
+ isPlenumSession: false,
+ status: "Accepted",
+ liveURL: null,
+ recordingURL: null,
+ isInformed: true,
+ isConfirmed: true,
+ roomID: faker.number.int(),
+ categories: [
+ {
+ id: 1,
+ name: "Session format",
+ sort: 1,
+ categoryItems: [
+ {
+ id: 1,
+ name: "Category 1",
+ },
+ ],
+ },
+ ],
+ questionAnswers: [
+ {
+ id: 1,
+ sort: 0,
+ question: "Tags/Topics",
+ answer: "Answer 1",
+ answerExtra: null,
+ questionType: "Short_Text",
+ },
+ ],
+ };
+
+ const result = talkCardAdapter(session, "2023");
+
+ expect(result.talk.title).toBe(session.title);
+ expect(result.talk.speakers.at(0)?.name).toBe(session.speakers[0].name);
+ expect(result.year).toBe("2023"); // Provided year
});
});
diff --git a/src/views/Talks/TalkCardAdapter.ts b/src/views/Talks/TalkCardAdapter.ts
index ed157ac89..9481e015e 100644
--- a/src/views/Talks/TalkCardAdapter.ts
+++ b/src/views/Talks/TalkCardAdapter.ts
@@ -7,7 +7,7 @@ import {
SessionSpeaker
} from "../../types/sessions";
-export const talkCardAdapter = (session: UngroupedSession): TalkCardProps => {
+export const talkCardAdapter = (session: UngroupedSession, year: string = "2024"): TalkCardProps => {
return {
talk: {
id: parseInt(session.id),
@@ -32,6 +32,7 @@ export const talkCardAdapter = (session: UngroupedSession): TalkCardProps => {
answer: qa.answer,
})) as QuestionAnswers[],
},
+ year,
showTrack: true, // Default value, adjust as necessary
};
};
diff --git a/src/views/Talks/Talks.tsx b/src/views/Talks/Talks.tsx
index e767dbbab..be8251672 100644
--- a/src/views/Talks/Talks.tsx
+++ b/src/views/Talks/Talks.tsx
@@ -12,7 +12,7 @@ import {
StyledTitleIcon,
StyledWaveContainer,
} from "./Talks.style";
-import TrackInformation from "./components/TrackInformation";
+import TrackInformation from "../../components/common/TrackInformation";
import { useFetchTalks } from "../../hooks/useFetchTalks";
import * as Sentry from "@sentry/react";
import { Dropdown, DropdownChangeEvent } from "primereact/dropdown";
@@ -127,7 +127,11 @@ const Talks: FC> = () => {
/>
{filteredTalks.map((track) => (
-
+
))}
>
)
diff --git a/src/views/Talks/components/TalkCard.tsx b/src/views/Talks/components/TalkCard.tsx
index a8d3a855b..c84a60374 100644
--- a/src/views/Talks/components/TalkCard.tsx
+++ b/src/views/Talks/components/TalkCard.tsx
@@ -1,98 +1,8 @@
-import React, {FC} from "react";
-import {Link} from "react-router";
-import {StyledJobsInfo} from "../../../components/JobOffers/JobsCard";
-import {Tag} from "../../../components/Tag/Tag";
-import {
- ROUTE_SPEAKER_DETAIL,
- ROUTE_TALK_DETAIL,
-} from "../../../constants/routes";
-import {
- StyledSessionCard,
- StyledSessionText,
- StyledTagsWrapper,
- StyledTalkSpeaker,
- StyledTalkTitle,
- StyledVoteTalkLink,
-} from "../Talks.style";
-import {Color} from "../../../styles/colors";
-import {
- extractSessionCategoryInfo,
- extractSessionTags
-} from "../../../services/sessionsAdapter";
-import {
- CategoryItemEnum,
- QuestionAnswers,
- SessionCategory,
- SessionSpeaker
-} from "../../../types/sessions";
+import React from "react";
+import CommonTalkCard, { TalkCardProps } from "../../../components/common/TalkCard";
-export interface TalkCardProps {
- talk: {
- id: number;
- title: string;
- talkImage?: number;
- speakers: SessionSpeaker[];
- level?: string;
- link?: string;
- tags?: string[];
- track: string;
- categories: SessionCategory[];
- questionAnswers: QuestionAnswers[];
- };
- showTrack?: boolean;
-}
+export type { TalkCardProps };
-export const TalkCard: FC> = ({
- showTrack = false,
- talk,
- }) => {
- return (
-
-
-
- {talk.title}
-
-
- {talk.speakers.map((speaker: SessionSpeaker) => (
-
-
- {speaker.name}
-
-
- ))}
-
-
- {`${extractSessionCategoryInfo(
- talk.categories,
- CategoryItemEnum.Format,
- )} `}
- {extractSessionCategoryInfo(talk.categories)}{" "}
-
- {showTrack && (
-
- Track:
- {extractSessionCategoryInfo(
- talk.categories,
- CategoryItemEnum.Track,
- )}
-
- )}
-
- {extractSessionTags(talk.questionAnswers)?.map((tag) => {
- return ;
- })}
-
-
-
- 🗳️ Vote this talk
-
-
-
-
- );
+export const TalkCard: React.FC> = (props) => {
+ return ;
};
From 13ef26d4e2d4873d6072c5039a06b4c53f981f18 Mon Sep 17 00:00:00 2001
From: Anyul Rivas
Date: Wed, 26 Mar 2025 16:51:33 +0100
Subject: [PATCH 11/11] test: add tests for speaker sections
---
src/2023/Diversity/Diversity.test.tsx | 8 +-
src/2023/Diversity/Diversity2023.tsx | 52 ++--
.../SessionFeedback/SessionFeedback2023.tsx | 7 +-
src/2023/Speakers/Speakers2023.test.tsx | 207 ++++++++++++
src/2023/Speakers/Speakers2023.tsx | 34 +-
.../Speakers/components/SpeakerCard.Style.tsx | 61 ----
src/2023/Speakers/components/SpeakersCard.tsx | 37 ---
src/2023/Talks/Talks.style.ts | 20 +-
.../SpeakerDetailContainer2024.tsx | 78 ++---
src/2024/Speakers/Speakers2024.test.tsx | 200 ++++++++++++
src/2024/Speakers/Speakers2024.tsx | 294 +++++++++---------
src/App.test.tsx | 6 +-
src/utils/testing/speakerTestUtils.tsx | 109 +++++++
.../Speakers/SpeakerInformation.test.tsx | 2 +-
src/views/Speakers/Speakers.test.tsx | 167 ++++++++++
src/views/Speakers/Speakers.tsx | 34 +-
.../Speakers/components/SpeakersCard.test.tsx | 60 ++++
.../Speakers/components/SpeakersCard.tsx | 26 +-
18 files changed, 1043 insertions(+), 359 deletions(-)
create mode 100644 src/2023/Speakers/Speakers2023.test.tsx
delete mode 100644 src/2023/Speakers/components/SpeakerCard.Style.tsx
delete mode 100644 src/2023/Speakers/components/SpeakersCard.tsx
create mode 100644 src/2024/Speakers/Speakers2024.test.tsx
create mode 100644 src/utils/testing/speakerTestUtils.tsx
create mode 100644 src/views/Speakers/Speakers.test.tsx
create mode 100644 src/views/Speakers/components/SpeakersCard.test.tsx
diff --git a/src/2023/Diversity/Diversity.test.tsx b/src/2023/Diversity/Diversity.test.tsx
index 765465529..9a30d1177 100644
--- a/src/2023/Diversity/Diversity.test.tsx
+++ b/src/2023/Diversity/Diversity.test.tsx
@@ -11,7 +11,7 @@ describe("Diversity component", () => {
} />
,
- { wrapper: BrowserRouter }
+ { wrapper: BrowserRouter },
);
const headingElement = screen.getByText("Diversity Sponsorship");
expect(headingElement).toBeInTheDocument();
@@ -24,10 +24,10 @@ describe("Diversity component", () => {
} />
,
- { wrapper: BrowserRouter }
+ { wrapper: BrowserRouter },
);
const paragraphElement = screen.getByText(
- /DevBcn, its volunteers, and staff consider that understanding/i
+ /DevBcn, its volunteers, and staff consider that understanding/i,
);
expect(paragraphElement).toBeInTheDocument();
});
@@ -39,7 +39,7 @@ describe("Diversity component", () => {
} />
,
- { wrapper: BrowserRouter }
+ { wrapper: BrowserRouter },
);
const vepeeLogo = screen.getByAltText("Vepee");
const adevintaLogo = screen.getByAltText("Adevinta");
diff --git a/src/2023/Diversity/Diversity2023.tsx b/src/2023/Diversity/Diversity2023.tsx
index 12e72bb11..abe51093c 100644
--- a/src/2023/Diversity/Diversity2023.tsx
+++ b/src/2023/Diversity/Diversity2023.tsx
@@ -10,23 +10,23 @@ import {
} from "../../constants/routes";
const StyledSection = styled.section`
- {
+{
padding-top: 48px;
- }
+}
- .top {
- clip-path: polygon(0 0, 100% 0, 100% 100%, 0 calc(100% - 50px));
- height: 51px;
- background-color: ${Color.DARK_BLUE};
- border-top: 1px solid ${Color.DARK_BLUE};
- }
+ .top {
+ clip-path: polygon(0 0, 100% 0, 100% 100%, 0 calc(100% - 50px));
+ height: 51px;
+ background-color: ${Color.DARK_BLUE};
+ border-top: 1px solid ${Color.DARK_BLUE};
+ }
- .bottom {
- clip-path: polygon(0 0, 100% 50px, 100% 100%, 0 100%);
- margin-top: -50px;
- height: 50px;
- background-color: ${Color.WHITE};
- }
+ .bottom {
+ clip-path: polygon(0 0, 100% 50px, 100% 100%, 0 100%);
+ margin-top: -50px;
+ height: 50px;
+ background-color: ${Color.WHITE};
+ }
`;
const StyledWave = styled.section`
@@ -36,15 +36,15 @@ const StyledWave = styled.section`
`;
const StyledLogo = styled.img`
- {
+{
max-width: 30vw;
flex: 2 1 auto;
padding-bottom: 50px;
- }
- @media only screen and (max-width: ${BIG_BREAKPOINT}px) {
- padding-bottom: 20px;
- max-width: 65vw;
- }
+}
+ @media only screen and (max-width: ${BIG_BREAKPOINT}px) {
+ padding-bottom: 20px;
+ max-width: 65vw;
+ }
`;
const Heading = styled.h1`
@@ -68,17 +68,17 @@ const StyledP = styled.p`
`;
const FlexDiv = styled.div`
- {
+{
display: flex;
width: 20%;
margin: 0 auto;
flex-direction: column;
padding-bottom: 20px;
- }
- @media only screen and (max-width: ${BIG_BREAKPOINT}px) {
- width: 60%;
- padding-bottom: 0.5rem;
- }
+}
+ @media only screen and (max-width: ${BIG_BREAKPOINT}px) {
+ width: 60%;
+ padding-bottom: 0.5rem;
+ }
`;
const StyledParagraph = styled.section`
diff --git a/src/2023/SessionFeedback/SessionFeedback2023.tsx b/src/2023/SessionFeedback/SessionFeedback2023.tsx
index 0f53ef14f..d40132e4d 100644
--- a/src/2023/SessionFeedback/SessionFeedback2023.tsx
+++ b/src/2023/SessionFeedback/SessionFeedback2023.tsx
@@ -18,9 +18,10 @@ import { ROUTE_TALK_DETAIL } from "../../constants/routes";
const SessionFeedback2023: FC> = () => {
const bodyTemplate = React.useCallback(
- (field: keyof MeasurableSessionRating) => (session: SessionRating) =>
- ,
- []
+ (field: keyof MeasurableSessionRating) => (session: SessionRating) => (
+
+ ),
+ [],
);
const TitleTemplate = (session: SessionRating) =>
diff --git a/src/2023/Speakers/Speakers2023.test.tsx b/src/2023/Speakers/Speakers2023.test.tsx
new file mode 100644
index 000000000..071df68cf
--- /dev/null
+++ b/src/2023/Speakers/Speakers2023.test.tsx
@@ -0,0 +1,207 @@
+import React from "react";
+import { screen } from "@testing-library/react";
+import Speakers2023 from "./Speakers2023";
+import {
+ createMockSpeakers,
+ renderWithRouterAndQueryClient,
+} from "../../utils/testing/speakerTestUtils";
+import { useFetchSpeakers } from "../../hooks/useFetchSpeakers";
+import userEvent from "@testing-library/user-event";
+import { gaEventTracker } from "../../components/analytics/Analytics";
+
+// Mock the useFetchSpeakers hook
+jest.mock("../../hooks/useFetchSpeakers");
+const mockedUseFetchSpeakers = useFetchSpeakers as jest.MockedFunction<
+ typeof useFetchSpeakers
+>;
+
+// Mock the gaEventTracker
+jest.mock("../../components/analytics/Analytics", () => ({
+ gaEventTracker: jest.fn(),
+}));
+
+// Mock the useWindowSize hook
+jest.mock("react-use", () => ({
+ useWindowSize: jest.fn(),
+}));
+
+// Mock Sentry
+jest.mock("@sentry/react", () => ({
+ captureException: jest.fn(),
+}));
+
+// Mock the 2023.json data
+jest.mock("../../data/2023.json", () => ({
+ hideSpeakers: false,
+ edition: "2023",
+ title: "DevBcn",
+ cfp: {
+ startDay: "2022-11-01T00:00:00",
+ endDay: "2023-03-15T00:00:00",
+ link: "https://sessionize.com/devbcn23/",
+ },
+}));
+
+describe("Speakers2023 component", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ require("react-use").useWindowSize.mockReturnValue({ width: 1200 });
+ });
+
+ it("displays loading state when data is being fetched", () => {
+ // Mock the hook to return loading state
+ mockedUseFetchSpeakers.mockReturnValue({
+ data: null,
+ isLoading: true,
+ error: null,
+ isSuccess: false,
+ });
+
+ renderWithRouterAndQueryClient();
+
+ expect(screen.getByText("Loading...")).toBeInTheDocument();
+ });
+
+ it("displays speakers when data is loaded", () => {
+ const mockSpeakers = createMockSpeakers(3);
+
+ // Mock the hook to return success state with data
+ mockedUseFetchSpeakers.mockReturnValue({
+ data: mockSpeakers,
+ isLoading: false,
+ error: null,
+ isSuccess: true,
+ });
+
+ renderWithRouterAndQueryClient();
+
+ // Check that each speaker's name is displayed
+ mockSpeakers.forEach((speaker) => {
+ expect(screen.getByText(speaker.fullName)).toBeInTheDocument();
+ });
+ });
+
+ it("displays a message when no speakers are available", () => {
+ // Mock the hook to return success state with empty data
+ mockedUseFetchSpeakers.mockReturnValue({
+ data: [],
+ isLoading: false,
+ error: null,
+ isSuccess: true,
+ });
+
+ renderWithRouterAndQueryClient();
+
+ expect(screen.getByText(/No selected speakers yet/i)).toBeInTheDocument();
+ });
+
+ it.skip("displays CFP button when current date is within CFP period", () => {
+ // Mock the hook to return success state with data
+ mockedUseFetchSpeakers.mockReturnValue({
+ data: [],
+ isLoading: false,
+ error: null,
+ isSuccess: true,
+ });
+
+ // Mock Date.now to return a date within the CFP period
+ const originalDate = Date;
+ global.Date = class extends Date {
+ constructor() {
+ super();
+ }
+
+ static now() {
+ return new Date("2023-01-15").getTime();
+ }
+ } as typeof Date;
+
+ renderWithRouterAndQueryClient();
+
+ const cfpButton = screen.getByText(/Apply to be a Speaker/i);
+ expect(cfpButton).toBeInTheDocument();
+
+ // Restore original Date
+ global.Date = originalDate;
+ });
+
+ it.skip("tracks CFP button clicks", async () => {
+ // Mock the hook to return success state with data
+ mockedUseFetchSpeakers.mockReturnValue({
+ data: [],
+ isLoading: false,
+ error: null,
+ isSuccess: true,
+ });
+
+ // Mock Date.now to return a date within the CFP period
+ const originalDate = Date;
+ global.Date = class extends Date {
+ constructor() {
+ super();
+ }
+
+ static now() {
+ return new Date("2023-01-15").getTime();
+ }
+ } as typeof Date;
+
+ renderWithRouterAndQueryClient();
+
+ const cfpButton = screen.getByText(/Apply to be a Speaker/i);
+ await userEvent.click(cfpButton);
+
+ expect(gaEventTracker).toHaveBeenCalledWith("CFP", "CFP");
+
+ // Restore original Date
+ global.Date = originalDate;
+ });
+
+ it("calls useFetchSpeakers with the correct year", () => {
+ // Mock the hook to return loading state
+ mockedUseFetchSpeakers.mockReturnValue({
+ data: null,
+ isLoading: true,
+ error: null,
+ isSuccess: false,
+ });
+
+ renderWithRouterAndQueryClient();
+
+ // Verify that useFetchSpeakers was called with "2023"
+ expect(mockedUseFetchSpeakers).toHaveBeenCalledWith("2023");
+ });
+
+ it("sets the document title correctly", () => {
+ // Mock the hook to return loading state
+ mockedUseFetchSpeakers.mockReturnValue({
+ data: null,
+ isLoading: true,
+ error: null,
+ isSuccess: false,
+ });
+
+ renderWithRouterAndQueryClient();
+
+ // Verify that document.title was set correctly
+ expect(document.title).toContain("Speakers2023");
+ expect(document.title).toContain("2023");
+ });
+
+ it("handles errors correctly", () => {
+ // Mock the hook to return error state
+ const error = new Error("Failed to fetch speakers");
+ mockedUseFetchSpeakers.mockReturnValue({
+ data: null,
+ isLoading: false,
+ error,
+ isSuccess: false,
+ });
+
+ renderWithRouterAndQueryClient();
+
+ // Verify that Sentry.captureException was called with the error
+ const Sentry = require("@sentry/react");
+ expect(Sentry.captureException).toHaveBeenCalledWith(error);
+ });
+});
diff --git a/src/2023/Speakers/Speakers2023.tsx b/src/2023/Speakers/Speakers2023.tsx
index 1d3ed6d4d..e00bfaa6d 100644
--- a/src/2023/Speakers/Speakers2023.tsx
+++ b/src/2023/Speakers/Speakers2023.tsx
@@ -1,12 +1,11 @@
-import {MOBILE_BREAKPOINT} from "../../constants/BreakPoints";
-import {Color} from "../../styles/colors";
-import {FC, useCallback, useEffect} from "react";
+import { MOBILE_BREAKPOINT } from "../../constants/BreakPoints";
+import { Color } from "../../styles/colors";
+import { FC, useCallback, useEffect } from "react";
import LessThanBlueIcon from "../../assets/images/LessThanBlueIcon.svg";
import MoreThanBlueIcon from "../../assets/images/MoreThanBlueIcon.svg";
import SectionWrapper from "../../components/SectionWrapper/SectionWrapper";
-import {SpeakerCard} from "./components/SpeakersCard";
import TitleSection from "../../components/SectionTitle/TitleSection";
-import {useWindowSize} from "react-use";
+import { useWindowSize } from "react-use";
import {
SpeakersCardsContainer,
StyledContainerLeftSlash,
@@ -19,10 +18,11 @@ import {
} from "./Speakers.style";
import webData from "../../data/2023.json";
import Button from "../../components/UI/Button";
-import {gaEventTracker} from "../../components/analytics/Analytics";
-import {useFetchSpeakers} from "../../hooks/useFetchSpeakers";
+import { gaEventTracker } from "../../components/analytics/Analytics";
+import { useFetchSpeakers } from "../../hooks/useFetchSpeakers";
import * as Sentry from "@sentry/react";
-import {ISpeaker} from "../../types/speakers";
+import { ISpeaker } from "../../types/speakers";
+import { SpeakerCard } from "../../views/Speakers/components/SpeakersCard";
const LessThanGreaterThan = (props: { width: number }) => (
<>
@@ -94,7 +94,11 @@ const Speakers2023: FC> = () => {
)}
{data?.map((speaker: ISpeaker) => (
-
+
))}
> = () => {
>
/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
- / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /{" "}
+ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
+ /{" "}
@@ -117,7 +122,8 @@ const Speakers2023: FC> = () => {
>
/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
- / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /{" "}
+ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
+ /{" "}
@@ -129,7 +135,8 @@ const Speakers2023: FC> = () => {
>
/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
- / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /{" "}
+ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
+ /{" "}
@@ -141,7 +148,8 @@ const Speakers2023: FC> = () => {
>
/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
- / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /{" "}
+ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
+ /{" "}
diff --git a/src/2023/Speakers/components/SpeakerCard.Style.tsx b/src/2023/Speakers/components/SpeakerCard.Style.tsx
deleted file mode 100644
index e226c2323..000000000
--- a/src/2023/Speakers/components/SpeakerCard.Style.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import styled from "styled-components";
-import {
- MOBILE_BREAKPOINT,
- TABLET_BREAKPOINT,
-} from "../../../constants/BreakPoints";
-import { Color } from "../../../styles/colors";
-
-export const StyledSpeakerCard = styled.div`
- display: flex;
- flex-direction: column;
- align-items: flex-start;
- width: 10rem;
- padding: 0 1rem 1rem 1rem;
-
- @media (min-width: ${TABLET_BREAKPOINT}px) {
- width: 12rem;
- }
- @media (min-width: ${MOBILE_BREAKPOINT}px) {
- width: 15rem;
- }
-`;
-export const StyledSpeakerImageContainer = styled.div`
- width: 100%;
- height: auto;
- position: relative;
-`;
-export const StyledSpeakerImage = styled.img`
- width: 100%;
- height: auto;
- display: block;
- border-radius: 10px;
-`;
-export const StyledImageAnimation = styled.div`
- position: absolute;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
- height: 100%;
- width: 100%;
- opacity: 0;
- transition: 0.25s linear;
- background-color: rgba(239, 71, 111, 0.5);
-
- &:hover {
- opacity: 1;
- }
-`;
-export const StyledSpeakerTitle = styled.h5`
- font-family: "DejaVu Sans ExtraLight", sans-serif;
- font-weight: bold;
- color: ${Color.LIGHT_BLUE};
- font-size: 1.1em;
- padding: 5px 0 1px;
-`;
-export const StyledSpeakerText = styled.p`
- color: ${Color.WHITE};
- font-family: "Square 721 Regular", sans-serif;
- text-align: left;
- font-size: 0.9em;
-`;
diff --git a/src/2023/Speakers/components/SpeakersCard.tsx b/src/2023/Speakers/components/SpeakersCard.tsx
deleted file mode 100644
index c5e6f9fa3..000000000
--- a/src/2023/Speakers/components/SpeakersCard.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import {FC, Suspense} from "react";
-import {
- StyledImageAnimation,
- StyledSpeakerCard,
- StyledSpeakerImage,
- StyledSpeakerImageContainer,
- StyledSpeakerText,
- StyledSpeakerTitle,
-} from "./SpeakerCard.Style";
-import {Link} from "react-router";
-import Loading from "../../../assets/images/logo.png";
-import {ROUTE_2023_SPEAKER_DETAIL} from "../../../constants/routes";
-import {ISpeaker} from "../../../types/speakers";
-
-type SpeakersCardProps = {
- speaker: ISpeaker;
-};
-
-export const SpeakerCard: FC> = ({ speaker }) => {
- return (
-
-
-
-
-
-
-
-
- {speaker.fullName}
- {speaker.tagLine}
-
-
- );
-};
diff --git a/src/2023/Talks/Talks.style.ts b/src/2023/Talks/Talks.style.ts
index 93a95f0e9..b42bdd548 100644
--- a/src/2023/Talks/Talks.style.ts
+++ b/src/2023/Talks/Talks.style.ts
@@ -47,10 +47,15 @@ export const StyledSessionText = styled.div`
export const StyledSessionCard = styled.div`
align-items: center;
/*min-width: 20%;
- max-width: 50%;*/
+ max-width: 50%;*/
margin: 0.5rem 1rem 1rem;
flex-grow: 2;
- background: linear-gradient(-45deg, ${Color.DARK_BLUE}, ${Color.LIGHT_BLUE}, ${Color.DARK_BLUE});
+ background: linear-gradient(
+ -45deg,
+ ${Color.DARK_BLUE},
+ ${Color.LIGHT_BLUE},
+ ${Color.DARK_BLUE}
+ );
background-size: 200% 200%;
border-radius: 10px;
padding: 15px;
@@ -82,12 +87,12 @@ export const StyledTalkTitle = styled(Link)`
}
`;
export const StyledTrackInfo = styled.h2`
- {
+{
color: ${Color.DARK_BLUE};
margin-top: 50px;
margin-left: 40px;
margin-bottom: 20px;
- }
+}
`;
export const StyledSessionSection = styled.section`
display: flex;
@@ -98,20 +103,23 @@ export const StyledSessionSection = styled.section`
export const StyledTalkSpeaker = styled.p`
font-size: 1em;
+
a:before {
content: "🧑🏻💻 ";
}
+
a {
text-decoration: none;
color: ${Color.WHITE};
}
+
a:hover {
color: ${Color.DARK_BLUE};
}
`;
export const StyledSelectTrack = styled.select`
- {
+{
padding: 5px;
color: ${Color.YELLOW};
background-color: ${Color.LIGHT_BLUE};
@@ -119,5 +127,5 @@ export const StyledSelectTrack = styled.select`
border: none;
font-size: 1.2em;
max-width: 15%;
- }
+}
`;
diff --git a/src/2024/SpeakerDetail/SpeakerDetailContainer2024.tsx b/src/2024/SpeakerDetail/SpeakerDetailContainer2024.tsx
index 8f3b838cd..1b4f12ded 100644
--- a/src/2024/SpeakerDetail/SpeakerDetailContainer2024.tsx
+++ b/src/2024/SpeakerDetail/SpeakerDetailContainer2024.tsx
@@ -1,52 +1,52 @@
-import {Color} from "../../styles/colors";
+import { Color } from "../../styles/colors";
-import React, {FC} from "react";
+import React, { FC } from "react";
import SectionWrapper from "../../components/SectionWrapper/SectionWrapper";
import SpeakerDetail from "./SpeakerDetail";
-import {useParams} from "react-router";
+import { useParams } from "react-router";
import conferenceData from "../../data/2024.json";
-import {useFetchSpeakers} from "../../hooks/useFetchSpeakers";
+import { useFetchSpeakers } from "../../hooks/useFetchSpeakers";
import * as Sentry from "@sentry/react";
-import {StyledContainer} from "../../views/SpeakerDetail/Speaker.style";
-import {StyledWaveContainer} from "../../views/Talks/Talks.style";
+import { StyledContainer } from "../../views/SpeakerDetail/Speaker.style";
+import { StyledWaveContainer } from "../../views/Talks/Talks.style";
const SpeakerDetailContainer2024: FC> = () => {
- const {id} = useParams<{ id: string }>();
+ const { id } = useParams<{ id: string }>();
- const {isLoading, error, data} = useFetchSpeakers("2024", id);
+ const { isLoading, error, data } = useFetchSpeakers("2024", id);
- if (error) {
- Sentry.captureException(error);
+ if (error) {
+ Sentry.captureException(error);
+ }
+ React.useEffect(() => {
+ if (data) {
+ document.title = `${data[0]?.fullName} - DevBcn - ${conferenceData.edition}`;
}
- React.useEffect(() => {
- if (data) {
- document.title = `${data[0]?.fullName} - DevBcn - ${conferenceData.edition}`;
- }
- }, [id, data]);
- return (
-
-
- {isLoading && Loading
}
- {!isLoading && data && data.length > 0 ? (
-
- ) : (
- "not found"
- )}
-
-
-
-
-
- );
+ }, [id, data]);
+ return (
+
+
+ {isLoading && Loading
}
+ {!isLoading && data && data.length > 0 ? (
+
+ ) : (
+ "not found"
+ )}
+
+
+
+
+
+ );
};
export default SpeakerDetailContainer2024;
diff --git a/src/2024/Speakers/Speakers2024.test.tsx b/src/2024/Speakers/Speakers2024.test.tsx
new file mode 100644
index 000000000..f0364bf9b
--- /dev/null
+++ b/src/2024/Speakers/Speakers2024.test.tsx
@@ -0,0 +1,200 @@
+import React from "react";
+import { screen } from "@testing-library/react";
+import Speakers2024 from "./Speakers2024";
+import {
+ createMockSpeakers,
+ renderWithRouterAndQueryClient,
+} from "../../utils/testing/speakerTestUtils";
+import { useFetchSpeakers } from "../../hooks/useFetchSpeakers";
+import userEvent from "@testing-library/user-event";
+import { gaEventTracker } from "../../components/analytics/Analytics";
+
+// Mock the useFetchSpeakers hook
+jest.mock("../../hooks/useFetchSpeakers");
+const mockedUseFetchSpeakers = useFetchSpeakers as jest.MockedFunction<
+ typeof useFetchSpeakers
+>;
+
+// Mock the gaEventTracker
+jest.mock("../../components/analytics/Analytics", () => ({
+ gaEventTracker: jest.fn(),
+}));
+
+// Mock the useWindowSize hook
+jest.mock("react-use", () => ({
+ useWindowSize: jest.fn(),
+}));
+
+// Mock Sentry
+jest.mock("@sentry/react", () => ({
+ captureException: jest.fn(),
+}));
+
+// Mock the 2024.json data
+jest.mock("../../data/2024.json", () => ({
+ hideSpeakers: false,
+ edition: "2024",
+ title: "DevBcn",
+ cfp: {
+ startDay: "2023-01-01T00:00:00",
+ endDay: "2023-02-01T00:00:00",
+ link: "https://example.com/cfp",
+ },
+}));
+
+describe("Speakers2024 component", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ require("react-use").useWindowSize.mockReturnValue({ width: 1200 });
+ });
+
+ it("displays loading state when data is being fetched", () => {
+ // Mock the hook to return loading state
+ mockedUseFetchSpeakers.mockReturnValue({
+ data: null,
+ isLoading: true,
+ error: null,
+ isSuccess: false,
+ });
+
+ renderWithRouterAndQueryClient();
+
+ expect(screen.getByText("Loading...")).toBeInTheDocument();
+ });
+
+ it("displays speakers when data is loaded", () => {
+ const mockSpeakers = createMockSpeakers(3);
+
+ // Mock the hook to return success state with data
+ mockedUseFetchSpeakers.mockReturnValue({
+ data: mockSpeakers,
+ isLoading: false,
+ error: null,
+ isSuccess: true,
+ });
+
+ renderWithRouterAndQueryClient();
+
+ // Check that each speaker's name is displayed
+ mockSpeakers.forEach((speaker) => {
+ expect(screen.getByText(speaker.fullName)).toBeInTheDocument();
+ });
+ });
+
+ it("displays a message when hideSpeakers is true", () => {
+ // Mock the hook to return success state with data
+ mockedUseFetchSpeakers.mockReturnValue({
+ data: [],
+ isLoading: false,
+ error: null,
+ isSuccess: true,
+ });
+
+ // Temporarily override the hideSpeakers value
+ const originalModule = jest.requireMock("../../data/2024.json");
+ const originalHideSpeakers = originalModule.hideSpeakers;
+ originalModule.hideSpeakers = true;
+
+ renderWithRouterAndQueryClient();
+
+ expect(screen.getByText(/No selected speakers yet/i)).toBeInTheDocument();
+
+ // Restore the original value
+ originalModule.hideSpeakers = originalHideSpeakers;
+ });
+
+ it.skip("displays CFP button when current date is within CFP period", () => {
+ // Mock the hook to return success state with data
+ mockedUseFetchSpeakers.mockReturnValue({
+ data: [],
+ isLoading: false,
+ error: null,
+ isSuccess: true,
+ });
+
+ // Mock Date to return a date within the CFP period
+ const originalDate = Date;
+ const mockDate = new Date("2023-01-15");
+ global.Date = class extends Date {
+ constructor() {
+ return mockDate;
+ }
+
+ static now() {
+ return mockDate.getTime();
+ }
+ } as unknown as typeof Date;
+
+ renderWithRouterAndQueryClient();
+
+ const cfpButton = screen.getByText(/Apply to be a Speaker/i);
+ expect(cfpButton).toBeInTheDocument();
+
+ // Restore original Date
+ global.Date = originalDate;
+ });
+
+ it.skip("tracks CFP button clicks", async () => {
+ // Mock the hook to return success state with data
+ mockedUseFetchSpeakers.mockReturnValue({
+ data: [],
+ isLoading: false,
+ error: null,
+ isSuccess: true,
+ });
+
+ // Mock Date to return a date within the CFP period
+ const originalDate = Date;
+ const mockDate = new Date("2023-01-15");
+ global.Date = class extends Date {
+ constructor() {
+ return mockDate;
+ }
+
+ static now() {
+ return mockDate.getTime();
+ }
+ } as unknown as typeof Date;
+
+ renderWithRouterAndQueryClient();
+
+ const cfpButton = screen.getByText(/Apply to be a Speaker/i);
+ await userEvent.click(cfpButton);
+
+ expect(gaEventTracker).toHaveBeenCalledWith("CFP", "CFP");
+
+ // Restore original Date
+ global.Date = originalDate;
+ });
+
+ it("calls useFetchSpeakers with the correct year", () => {
+ // Mock the hook to return loading state
+ mockedUseFetchSpeakers.mockReturnValue({
+ data: null,
+ isLoading: true,
+ error: null,
+ isSuccess: false,
+ });
+
+ renderWithRouterAndQueryClient();
+
+ // Verify that useFetchSpeakers was called with "2024"
+ expect(mockedUseFetchSpeakers).toHaveBeenCalledWith("2024");
+ });
+
+ it("sets the document title correctly", () => {
+ // Mock the hook to return loading state
+ mockedUseFetchSpeakers.mockReturnValue({
+ data: null,
+ isLoading: true,
+ error: null,
+ isSuccess: false,
+ });
+
+ renderWithRouterAndQueryClient();
+
+ // Verify that document.title was set correctly
+ expect(document.title).toContain("Speakers");
+ expect(document.title).toContain("2024");
+ });
+});
diff --git a/src/2024/Speakers/Speakers2024.tsx b/src/2024/Speakers/Speakers2024.tsx
index 1b978018e..b70db59dc 100644
--- a/src/2024/Speakers/Speakers2024.tsx
+++ b/src/2024/Speakers/Speakers2024.tsx
@@ -1,176 +1,174 @@
-import {MOBILE_BREAKPOINT} from "../../constants/BreakPoints";
-import {Color} from "../../styles/colors";
-import React, {FC, useCallback, useEffect} from "react";
+import { MOBILE_BREAKPOINT } from "../../constants/BreakPoints";
+import { Color } from "../../styles/colors";
+import React, { FC, useCallback, useEffect } from "react";
import LessThanBlueIcon from "../../assets/images/LessThanBlueIcon.svg";
import MoreThanBlueIcon from "../../assets/images/MoreThanBlueIcon.svg";
import SectionWrapper from "../../components/SectionWrapper/SectionWrapper";
import TitleSection from "../../components/SectionTitle/TitleSection";
-import {useWindowSize} from "react-use";
+import { useWindowSize } from "react-use";
import {
- SpeakersCardsContainer,
- StyledContainerLeftSlash,
- StyledContainerRightSlash,
- StyledLessIcon,
- StyledMoreIcon,
- StyledSlash,
- StyledSpeakersSection,
- StyledWaveContainer,
+ SpeakersCardsContainer,
+ StyledContainerLeftSlash,
+ StyledContainerRightSlash,
+ StyledLessIcon,
+ StyledMoreIcon,
+ StyledSlash,
+ StyledSpeakersSection,
+ StyledWaveContainer,
} from "./Speakers.style";
import webData from "../../data/2024.json";
import Button from "../../components/UI/Button";
-import {gaEventTracker} from "../../components/analytics/Analytics";
-import {useFetchSpeakers} from "../../hooks/useFetchSpeakers";
+import { gaEventTracker } from "../../components/analytics/Analytics";
+import { useFetchSpeakers } from "../../hooks/useFetchSpeakers";
import * as Sentry from "@sentry/react";
-import {SpeakerCard} from "../../views/Speakers/components/SpeakersCard";
-import {ISpeaker} from "../../types/speakers";
+import { SpeakerCard } from "../../views/Speakers/components/SpeakersCard";
+import { ISpeaker } from "../../types/speakers";
const LessThanGreaterThan = (props: { width: number }) => (
- <>
- {props.width > MOBILE_BREAKPOINT && (
- <>
-
-
- >
- )}
- >
+ <>
+ {props.width > MOBILE_BREAKPOINT && (
+ <>
+
+
+ >
+ )}
+ >
);
const Speakers2024: FC> = () => {
- const {width} = useWindowSize();
- const today = new Date();
- const isBetween = (startDay: Date, endDay: Date): boolean =>
- startDay < new Date() && endDay > today;
+ const { width } = useWindowSize();
+ const today = new Date();
+ const isBetween = (startDay: Date, endDay: Date): boolean =>
+ startDay < new Date() && endDay > today;
- const {error, data, isLoading} = useFetchSpeakers("2024");
+ const { error, data, isLoading } = useFetchSpeakers("2024");
- if (error) {
- Sentry.captureException(error);
- }
+ if (error) {
+ Sentry.captureException(error);
+ }
- const trackCFP = useCallback(() => {
- gaEventTracker("CFP", "CFP");
- }, []);
+ const trackCFP = useCallback(() => {
+ gaEventTracker("CFP", "CFP");
+ }, []);
- useEffect(() => {
- document.title = `Speakers — ${webData.title} — ${webData.edition}`;
- });
+ useEffect(() => {
+ document.title = `Speakers — ${webData.title} — ${webData.edition}`;
+ });
- const CFPStartDay = new Date(webData.cfp.startDay);
- const CFPEndDay = new Date(webData.cfp.endDay);
- return (
- <>
-
-
-
-
-
- {isLoading && Loading...
}
- {isBetween(CFPStartDay, CFPEndDay) && (
-
-
-
- )}
- {webData.hideSpeakers ? (
-
- No selected speakers yet. Keep in touch in our
- social media for
- upcoming announcements
-
- ) : (
- data?.map((speaker: ISpeaker) => (
-
- ))
- )}
-
-
-
- / / / / / / / / / / / / / / / / / / / / / / / / / /
- / / / / / / /
- / / / / / / / / / / / / / / / / / / / / / / / / / /
- / / / / / / /{" "}
-
-
+ color={Color.WHITE}
+ />
+
+
+ {isLoading && Loading...
}
+ {isBetween(CFPStartDay, CFPEndDay) && (
+
+
+
+ )}
+ {webData.hideSpeakers ? (
+
+ No selected speakers yet. Keep in touch in our social media for
+ upcoming announcements
+
+ ) : (
+ data?.map((speaker: ISpeaker) => (
+
+ ))
+ )}
+
+
+
+ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
+ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
+ /{" "}
+
+
-
-
- / / / / / / / / / / / / / / / / / / / / / / / / / /
- / / / / / / /
- / / / / / / / / / / / / / / / / / / / / / / / / / /
- / / / / / / /{" "}
-
-
+
+
+ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
+ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
+ /{" "}
+
+
-
-
- / / / / / / / / / / / / / / / / / / / / / / / / / /
- / / / / / / /
- / / / / / / / / / / / / / / / / / / / / / / / / / /
- / / / / / / /{" "}
-
-
+
+
+ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
+ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
+ /{" "}
+
+
-
-
- / / / / / / / / / / / / / / / / / / / / / / / / / /
- / / / / / / /
- / / / / / / / / / / / / / / / / / / / / / / / / / /
- / / / / / / /{" "}
-
-
-
-
-
-
-
- >
- );
+
+
+ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
+ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
+ /{" "}
+
+
+
+
+
+
+
+ >
+ );
};
export default Speakers2024;
diff --git a/src/App.test.tsx b/src/App.test.tsx
index 9602e8d61..2da1c4718 100644
--- a/src/App.test.tsx
+++ b/src/App.test.tsx
@@ -90,9 +90,9 @@ describe("navigation pages", () => {
expect(() => screen.getByText("JOB OFFERS")).toThrow();
//const user = userEvent.setup();
/*await user.click(screen.getByText("JOB OFFERS"));
- expect(
- await screen.findByText("Have a look at some opportunities"),
- ).not.toBeInTheDocument();*/
+ expect(
+ await screen.findByText("Have a look at some opportunities"),
+ ).not.toBeInTheDocument();*/
});
//Reason: not enabled yet
diff --git a/src/utils/testing/speakerTestUtils.tsx b/src/utils/testing/speakerTestUtils.tsx
new file mode 100644
index 000000000..8c43b4b93
--- /dev/null
+++ b/src/utils/testing/speakerTestUtils.tsx
@@ -0,0 +1,109 @@
+import React, { Suspense } from "react";
+import { BrowserRouter, Route, Routes } from "react-router";
+import { ISpeaker } from "../../types/speakers";
+import { QueryClient, QueryClientProvider } from "react-query";
+import { render, RenderOptions, RenderResult } from "@testing-library/react";
+
+// Re-export everything from testing-library
+export * from "@testing-library/react";
+
+// Create mock speaker data
+export const createMockSpeaker = (overrides = {}): ISpeaker => ({
+ id: "1",
+ fullName: "John Smith",
+ speakerImage: "https://example.com/john.jpg",
+ tagLine: "Software engineer",
+ bio: "I am a software engineer",
+ sessions: [
+ {
+ id: 4567,
+ name: "sample session",
+ },
+ ],
+ links: [
+ {
+ linkType: "Twitter",
+ url: "https://twitter.com/johnsmith",
+ title: "",
+ },
+ {
+ linkType: "LinkedIn",
+ url: "https://linkedin.com/in/johnsmith",
+ title: "",
+ },
+ ],
+ ...overrides,
+});
+
+// Create an array of mock speakers
+export const createMockSpeakers = (count: number): ISpeaker[] => {
+ return Array.from({ length: count }, (_, i) =>
+ createMockSpeaker({
+ id: `${i + 1}`,
+ fullName: `Speaker ${i + 1}`,
+ speakerImage: `https://example.com/speaker${i + 1}.jpg`,
+ tagLine: `Tagline for Speaker ${i + 1}`,
+ }),
+ );
+};
+
+// Create a custom render function that includes the BrowserRouter
+export function renderWithRouter(
+ ui: React.ReactElement,
+ options?: Omit,
+): RenderResult {
+ const wrapper: React.FC> = ({ children }) => (
+
+ Loading...}>
+
+
+
+
+
+ );
+
+ return render(ui, { wrapper, ...options });
+}
+
+// Create a custom render function that includes both BrowserRouter and QueryClientProvider
+export function renderWithRouterAndQueryClient(
+ ui: React.ReactElement,
+ options?: Omit,
+): RenderResult {
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ },
+ },
+ });
+
+ const wrapper: React.FC> = ({ children }) => (
+
+
+ Loading...}>
+
+
+
+
+
+
+ );
+
+ return render(ui, { wrapper, ...options });
+}
+
+// Mock the useFetchSpeakers hook
+export const mockUseFetchSpeakers = (
+ data: ISpeaker[] | null = null,
+ isLoading = false,
+ error: Error | null = null,
+ isSuccess = !isLoading && !error,
+) => {
+ return {
+ data,
+ isLoading,
+ error,
+ isSuccess,
+ };
+};
diff --git a/src/views/Speakers/SpeakerInformation.test.tsx b/src/views/Speakers/SpeakerInformation.test.tsx
index 280496e0b..b06c6727f 100644
--- a/src/views/Speakers/SpeakerInformation.test.tsx
+++ b/src/views/Speakers/SpeakerInformation.test.tsx
@@ -11,7 +11,7 @@ describe("Speakers activities component", () => {
} />
,
- { wrapper: BrowserRouter }
+ { wrapper: BrowserRouter },
);
const headingElement = screen.getByText("Speakers activities plan");
expect(headingElement).toBeInTheDocument();
diff --git a/src/views/Speakers/Speakers.test.tsx b/src/views/Speakers/Speakers.test.tsx
new file mode 100644
index 000000000..cd15390c5
--- /dev/null
+++ b/src/views/Speakers/Speakers.test.tsx
@@ -0,0 +1,167 @@
+import React from "react";
+import { screen } from "@testing-library/react";
+import Speakers from "./Speakers";
+import {
+ createMockSpeakers,
+ renderWithRouterAndQueryClient,
+} from "../../utils/testing/speakerTestUtils";
+import { useFetchSpeakers } from "../../hooks/useFetchSpeakers";
+import userEvent from "@testing-library/user-event";
+import { gaEventTracker } from "../../components/analytics/Analytics";
+
+// Mock the useFetchSpeakers hook
+jest.mock("../../hooks/useFetchSpeakers");
+const mockedUseFetchSpeakers = useFetchSpeakers as jest.MockedFunction<
+ typeof useFetchSpeakers
+>;
+
+// Mock the gaEventTracker
+jest.mock("../../components/analytics/Analytics", () => ({
+ gaEventTracker: jest.fn(),
+}));
+
+// Mock the useWindowSize hook
+jest.mock("react-use", () => ({
+ useWindowSize: jest.fn(),
+}));
+
+// Mock Sentry
+jest.mock("@sentry/react", () => ({
+ captureException: jest.fn(),
+}));
+
+// Mock the 2024.json data
+jest.mock("../../data/2024.json", () => ({
+ hideSpeakers: false,
+ edition: "2024",
+ title: "DevBcn",
+ cfp: {
+ startDay: "2023-01-01T00:00:00",
+ endDay: "2023-02-01T00:00:00",
+ link: "https://example.com/cfp",
+ },
+}));
+
+describe("Speakers component", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ require("react-use").useWindowSize.mockReturnValue({ width: 1200 });
+ });
+
+ it("displays loading state when data is being fetched", () => {
+ // Mock the hook to return loading state
+ mockedUseFetchSpeakers.mockReturnValue({
+ data: null,
+ isLoading: true,
+ error: null,
+ isSuccess: false,
+ });
+
+ renderWithRouterAndQueryClient();
+
+ expect(screen.getByText("Loading...")).toBeInTheDocument();
+ });
+
+ it("displays speakers when data is loaded", () => {
+ const mockSpeakers = createMockSpeakers(3);
+
+ // Mock the hook to return success state with data
+ mockedUseFetchSpeakers.mockReturnValue({
+ data: mockSpeakers,
+ isLoading: false,
+ error: null,
+ isSuccess: true,
+ });
+
+ renderWithRouterAndQueryClient();
+
+ // Check that each speaker's name is displayed
+ mockSpeakers.forEach((speaker) => {
+ expect(screen.getByText(speaker.fullName)).toBeInTheDocument();
+ });
+ });
+
+ it("displays a message when speakers are hidden", () => {
+ // Mock the hook to return success state with data
+ mockedUseFetchSpeakers.mockReturnValue({
+ data: [],
+ isLoading: false,
+ error: null,
+ isSuccess: true,
+ });
+
+ // Temporarily override the hideSpeakers value
+ const originalModule = jest.requireMock("../../data/2024.json");
+ const originalHideSpeakers = originalModule.hideSpeakers;
+ originalModule.hideSpeakers = true;
+
+ renderWithRouterAndQueryClient();
+
+ expect(screen.getByText(/No selected speakers yet/i)).toBeInTheDocument();
+
+ // Restore the original value
+ originalModule.hideSpeakers = originalHideSpeakers;
+ });
+
+ it.skip("displays CFP button when current date is within CFP period", () => {
+ // Mock the hook to return success state with data
+ mockedUseFetchSpeakers.mockReturnValue({
+ data: [],
+ isLoading: false,
+ error: null,
+ isSuccess: true,
+ });
+
+ // Mock Date.now to return a date within the CFP period
+ const originalDate = Date;
+ global.Date = class extends Date {
+ constructor() {
+ super();
+ }
+
+ static now() {
+ return new Date("2023-01-15").getTime();
+ }
+ } as typeof Date;
+
+ renderWithRouterAndQueryClient();
+
+ const cfpButton = screen.getByText(/Apply to be a Speaker/i);
+ expect(cfpButton).toBeInTheDocument();
+
+ // Restore original Date
+ global.Date = originalDate;
+ });
+
+ it.skip("tracks CFP button clicks", async () => {
+ // Mock the hook to return success state with data
+ mockedUseFetchSpeakers.mockReturnValue({
+ data: [],
+ isLoading: false,
+ error: null,
+ isSuccess: true,
+ });
+
+ // Mock Date.now to return a date within the CFP period
+ const originalDate = Date;
+ global.Date = class extends Date {
+ constructor() {
+ super();
+ }
+
+ static now() {
+ return new Date("2023-01-15").getTime();
+ }
+ } as typeof Date;
+
+ renderWithRouterAndQueryClient();
+
+ const cfpButton = screen.getByText(/Apply to be a Speaker/i);
+ await userEvent.click(cfpButton);
+
+ expect(gaEventTracker).toHaveBeenCalledWith("CFP", "CFP");
+
+ // Restore original Date
+ global.Date = originalDate;
+ });
+});
diff --git a/src/views/Speakers/Speakers.tsx b/src/views/Speakers/Speakers.tsx
index ee918abe3..cd9f446a3 100644
--- a/src/views/Speakers/Speakers.tsx
+++ b/src/views/Speakers/Speakers.tsx
@@ -1,12 +1,12 @@
-import {MOBILE_BREAKPOINT} from "../../constants/BreakPoints";
-import {Color} from "../../styles/colors";
-import {FC, useCallback, useEffect} from "react";
+import { MOBILE_BREAKPOINT } from "../../constants/BreakPoints";
+import { Color } from "../../styles/colors";
+import { FC, useCallback, useEffect } from "react";
import LessThanBlueIcon from "../../assets/images/LessThanBlueIcon.svg";
import MoreThanBlueIcon from "../../assets/images/MoreThanBlueIcon.svg";
import SectionWrapper from "../../components/SectionWrapper/SectionWrapper";
-import {SpeakerCard} from "./components/SpeakersCard";
+import { SpeakerCard } from "./components/SpeakersCard";
import TitleSection from "../../components/SectionTitle/TitleSection";
-import {useWindowSize} from "react-use";
+import { useWindowSize } from "react-use";
import {
SpeakersCardsContainer,
StyledContainerLeftSlash,
@@ -19,10 +19,10 @@ import {
} from "./Speakers.style";
import webData from "../../data/2024.json";
import Button from "../../components/UI/Button";
-import {gaEventTracker} from "../../components/analytics/Analytics";
-import {useFetchSpeakers} from "../../hooks/useFetchSpeakers";
+import { gaEventTracker } from "../../components/analytics/Analytics";
+import { useFetchSpeakers } from "../../hooks/useFetchSpeakers";
import * as Sentry from "@sentry/react";
-import {ISpeaker} from "../../types/speakers";
+import { ISpeaker } from "../../types/speakers";
const LessThanGreaterThan = (props: { width: number }) => (
<>
@@ -94,7 +94,11 @@ const Speakers: FC> = () => {
) : (
data?.map((speaker: ISpeaker) => (
-
+
))
)}
@@ -106,7 +110,8 @@ const Speakers: FC> = () => {
>
/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
- / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /{" "}
+ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
+ /{" "}
@@ -118,7 +123,8 @@ const Speakers: FC> = () => {
>
/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
- / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /{" "}
+ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
+ /{" "}
@@ -130,7 +136,8 @@ const Speakers: FC> = () => {
>
/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
- / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /{" "}
+ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
+ /{" "}
@@ -142,7 +149,8 @@ const Speakers: FC> = () => {
>
/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
- / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /{" "}
+ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
+ /{" "}
diff --git a/src/views/Speakers/components/SpeakersCard.test.tsx b/src/views/Speakers/components/SpeakersCard.test.tsx
new file mode 100644
index 000000000..dd9f5afaa
--- /dev/null
+++ b/src/views/Speakers/components/SpeakersCard.test.tsx
@@ -0,0 +1,60 @@
+import React from "react";
+import { screen } from "@testing-library/react";
+import { SpeakerCard, getSpeakerRoute } from "./SpeakersCard";
+import { createMockSpeaker, renderWithRouter } from "../../../utils/testing/speakerTestUtils";
+import { ROUTE_2023_SPEAKER_DETAIL, ROUTE_2024_SPEAKER_DETAIL, ROUTE_SPEAKER_DETAIL } from "../../../constants/routes";
+
+describe("SpeakerCard", () => {
+ const mockSpeaker = createMockSpeaker();
+
+ it("renders speaker information correctly", () => {
+ renderWithRouter();
+
+ // Check that the speaker's name and tagline are displayed
+ expect(screen.getByText(mockSpeaker.fullName)).toBeInTheDocument();
+ expect(screen.getByText(mockSpeaker.tagLine)).toBeInTheDocument();
+
+ // Check that the image is rendered with the correct src
+ const image = screen.getByRole("img");
+ expect(image).toHaveAttribute("src", mockSpeaker.speakerImage);
+ });
+
+ it("creates a link to the correct speaker detail page for 2024", () => {
+ renderWithRouter();
+
+ // Check that the link points to the correct route
+ const link = screen.getByRole("link");
+ expect(link).toHaveAttribute("href", `${ROUTE_2024_SPEAKER_DETAIL}/${mockSpeaker.id}`);
+ });
+
+ it("creates a link to the correct speaker detail page for 2023", () => {
+ renderWithRouter();
+
+ // Check that the link points to the correct route
+ const link = screen.getByRole("link");
+ expect(link).toHaveAttribute("href", `${ROUTE_2023_SPEAKER_DETAIL}/${mockSpeaker.id}`);
+ });
+
+ it("creates a link to the default speaker detail page for other years", () => {
+ renderWithRouter();
+
+ // Check that the link points to the correct route
+ const link = screen.getByRole("link");
+ expect(link).toHaveAttribute("href", `${ROUTE_SPEAKER_DETAIL}/${mockSpeaker.id}`);
+ });
+});
+
+describe("getSpeakerRoute", () => {
+ it("returns the 2023 route for year 2023", () => {
+ expect(getSpeakerRoute("2023")).toBe(ROUTE_2023_SPEAKER_DETAIL);
+ });
+
+ it("returns the 2024 route for year 2024", () => {
+ expect(getSpeakerRoute("2024")).toBe(ROUTE_2024_SPEAKER_DETAIL);
+ });
+
+ it("returns the default route for other years", () => {
+ expect(getSpeakerRoute("2022")).toBe(ROUTE_SPEAKER_DETAIL);
+ expect(getSpeakerRoute("")).toBe(ROUTE_SPEAKER_DETAIL);
+ });
+});
\ No newline at end of file
diff --git a/src/views/Speakers/components/SpeakersCard.tsx b/src/views/Speakers/components/SpeakersCard.tsx
index d2539640e..13bb6980d 100644
--- a/src/views/Speakers/components/SpeakersCard.tsx
+++ b/src/views/Speakers/components/SpeakersCard.tsx
@@ -1,4 +1,4 @@
-import {FC, Suspense} from "react";
+import { FC, Suspense } from "react";
import {
StyledImageAnimation,
StyledSpeakerCard,
@@ -7,22 +7,38 @@ import {
StyledSpeakerText,
StyledSpeakerTitle,
} from "./SpeakerCard.Style";
-import {Link} from "react-router";
-import {ROUTE_SPEAKER_DETAIL} from "../../../constants/routes";
+import { Link } from "react-router";
+import {
+ ROUTE_2023_SPEAKER_DETAIL,
+ ROUTE_2024_SPEAKER_DETAIL,
+ ROUTE_SPEAKER_DETAIL,
+} from "../../../constants/routes";
import Loading from "../../../assets/images/logo.png";
-import {ISpeaker} from "../../../types/speakers";
+import { ISpeaker } from "../../../types/speakers";
type SpeakerCardProps = {
speaker: ISpeaker;
+ year: string;
+};
+
+export const getSpeakerRoute = (year: string): string => {
+ if (year === "2023") {
+ return ROUTE_2023_SPEAKER_DETAIL;
+ }
+ if (year === "2024") {
+ return ROUTE_2024_SPEAKER_DETAIL;
+ }
+ return ROUTE_SPEAKER_DETAIL;
};
export const SpeakerCard: FC> = ({
speaker,
+ year,
}) => {
return (