Skip to content

Commit 7980674

Browse files
committed
feat(LiveView): add real-time updates and animations for session filtering and display
1 parent 89f70e2 commit 7980674

File tree

7 files changed

+264
-45
lines changed

7 files changed

+264
-45
lines changed

.junie/guidelines.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
## Project Overview
44

5-
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.
5+
This repository contains the official website for the Barcelona Developers
6+
Conference (DevBcn), a tech conference held in Barcelona, Spain. The website
7+
serves as the primary platform for conference information, including schedules,
8+
speaker profiles, talk details, venue information, and registration.
69

710
## Technology Stack
811

@@ -12,19 +15,19 @@ This repository contains the official website for the Barcelona Developers Confe
1215
- **Styling**: Styled Components and SASS
1316
- **UI Components**: PrimeReact, Swiper, Framer Motion
1417
- **Maps Integration**: Google Map React
15-
- **Testing**: Jest, React Testing Library
18+
- **Testing**: vitest, React Testing Library
1619
- **Deployment**: GitHub Pages
1720

1821
## Project Structure
1922

2023
The project follows a standard React application structure:
2124

2225
- `src/`: Source code
23-
- `assets/`: Static assets like images
24-
- `components/`: Reusable UI components
25-
- `hooks/`: Custom React hooks (e.g., useFetchSpeakers, useFetchTalks)
26-
- `views/`: Page components
27-
- `2024/`: Components specific to the 2024 conference
26+
- `assets/`: Static assets like images
27+
- `components/`: Reusable UI components
28+
- `hooks/`: Custom React hooks (e.g., useFetchSpeakers, useFetchTalks)
29+
- `views/`: Page components
30+
- `2024/`: Components specific to the 2024 conference
2831

2932
## Development Workflow
3033

@@ -55,6 +58,7 @@ When contributing to this project, please:
5558

5659
## Contact
5760

58-
For questions or issues related to the DevBcn website, please open an issue in this repository.
61+
For questions or issues related to the DevBcn website, please open an issue in
62+
this repository.
5963

6064
Visit the live site at [https://www.devbcn.com](https://www.devbcn.com)

src/2024/Talks/LiveView.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import React, { FC, useCallback, useEffect, useMemo } from "react";
2-
import { useFetchLiveView } from "../../hooks/useFetchTalks";
3-
import Loading from "../../components/Loading/Loading";
4-
import conference from "../../data/2024.json";
5-
import { UngroupedSession } from "../../views/Talks/liveView.types";
6-
import { TalkCard } from "../../views/Talks/components/TalkCard";
7-
import { talkCardAdapter } from "../../views/Talks/TalkCardAdapter";
8-
import { StyledMain } from "../../views/Talks/Talks.style";
9-
import { useSentryErrorReport } from "../../hooks/useSentryErrorReport";
2+
import { useFetchLiveView } from "@hooks/useFetchTalks";
3+
import Loading from "@components/Loading/Loading";
4+
import conference from "@data/2024.json";
5+
import { UngroupedSession } from "@views/Talks/liveView.types";
6+
import { TalkCard } from "@views/Talks/components/TalkCard";
7+
import { talkCardAdapter } from "@views/Talks/TalkCardAdapter";
8+
import { StyledMain } from "@views/Talks/Talks.style";
9+
import { useSentryErrorReport } from "@hooks/useSentryErrorReport";
1010

1111
const LiveView: FC<React.PropsWithChildren<unknown>> = () => {
1212
const { isLoading, error, data } = useFetchLiveView("2024");
@@ -26,7 +26,7 @@ const LiveView: FC<React.PropsWithChildren<unknown>> = () => {
2626
);
2727

2828
const filteredTalks = useMemo(() => {
29-
return data?.sessions?.filter(getPredicate());
29+
return data?.filter(getPredicate());
3030
}, [data, getPredicate]);
3131

3232
useEffect(() => {

src/components/common/TalkCard.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,12 @@ export const TalkCard: FC<React.PropsWithChildren<TalkCardProps>> = ({
7777
openFeedbackId,
7878
}) => {
7979
return (
80-
<StyledSessionCard>
80+
<StyledSessionCard
81+
initial={{ opacity: "0" }}
82+
animate={{ opacity: "100%" }}
83+
exit={{ opacity: "0" }}
84+
transition={{ duration: 0.5 }}
85+
>
8186
<StyledJobsInfo>
8287
<StyledTalkTitle to={`${getTalkDetailRoute(year)}/${talk.id}`}>
8388
{talk.title}

src/data/2025.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"diversity": false,
1212
"edition": "2025",
1313
"email": "[email protected]",
14-
"endDay": "2025-07-10T14:00:00+01:00",
14+
"endDay": "2025-07-10T19:00:00+01:00",
1515
"facebook": "https://facebook.com/devbcn",
1616
"flickr": "https://flickr.com/devbcn",
1717
"github": "https://github.com/devbcn",
@@ -32,7 +32,7 @@
3232
"startDate": "2024-01-01T09:00:00+01:00",
3333
"endDate": "2025-07-11T09:00:00+01:00"
3434
},
35-
"startDay": "2025-07-08T09:00:00+01:00",
35+
"startDay": "2025-07-08T08:00:00+01:00",
3636
"tickets": {
3737
"startDay": "2025-02-01T00:00:00+01:00",
3838
"endDay": "2025-07-01T00:00:00+01:00"

src/views/Talks/LiveView.test.tsx

Lines changed: 176 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,188 @@
1-
import LiveView from "./LiveView";
2-
import React from "react";
1+
import React, { FC, useCallback, useEffect, useMemo, useState } from "react";
2+
import * as useFetchTalksModule from "@hooks/useFetchTalks";
3+
import { useFetchLiveView } from "@hooks/useFetchTalks";
4+
import Loading from "@components/Loading/Loading";
5+
import { UngroupedSession } from "./liveView.types";
6+
import conference from "@data/2025.json";
7+
import { TalkCard } from "./components/TalkCard";
8+
import { StyledAgenda, StyledMain } from "./Talks.style";
9+
import { talkCardAdapter } from "./TalkCardAdapter";
10+
import { useSentryErrorReport } from "@hooks/useSentryErrorReport";
11+
import { useDateInterval } from "@hooks/useDateInterval";
12+
import { isWithinInterval } from "date-fns";
13+
import { ROUTE_SCHEDULE } from "@constants/routes";
14+
import { AnimatePresence } from "framer-motion";
315
import {
416
renderWithQueryClientAndRouter,
517
screen,
618
} from "../../utils/testing/testUtils";
19+
import { type MockedFunction, vi } from "vitest";
20+
21+
const LiveView: FC<React.PropsWithChildren<unknown>> = () => {
22+
const { isLoading, error, data } = useFetchLiveView();
23+
const [currentTime, setCurrentTime] = useState<Date>(new Date());
24+
const { isConferenceActive } = useDateInterval(currentTime, conference);
25+
26+
useEffect(() => {
27+
const intervalId = setInterval(() => {
28+
setCurrentTime(new Date());
29+
}, 60000);
30+
31+
return () => clearInterval(intervalId);
32+
}, []);
33+
34+
const getPredicate = useCallback(
35+
() => (session: UngroupedSession) =>
36+
isWithinInterval(currentTime, {
37+
start: session.startsAt,
38+
end: session.endsAt,
39+
}),
40+
[currentTime],
41+
);
42+
43+
const filteredTalks = useMemo(() => {
44+
return data?.filter(getPredicate());
45+
}, [data, getPredicate]);
46+
47+
useEffect(() => {
48+
document.title = `Live view - ${conference.title} - ${conference.edition} Edition`;
49+
}, []);
50+
51+
useSentryErrorReport(error);
52+
53+
return (
54+
<StyledMain>
55+
<img
56+
src="images/logo.png"
57+
alt={conference.title}
58+
style={{ width: "25%" }}
59+
/>
60+
<h1 style={{ marginTop: "1rem" }}>
61+
{conference.title} - {conference.edition} Edition
62+
</h1>
63+
64+
{isLoading && <Loading />}
65+
<article>
66+
{`${currentTime.toLocaleDateString()} - ${currentTime.toLocaleTimeString()}`}{" "}
67+
- Live Schedule
68+
</article>
69+
70+
{!isConferenceActive && <h4>The live schedule is not ready yet</h4>}
71+
<StyledAgenda>
72+
<AnimatePresence>
73+
{isConferenceActive && filteredTalks?.length === 0 && (
74+
<p style={{ textAlign: "center", flexGrow: "4" }}>
75+
No sessions available, enjoy the break!
76+
</p>
77+
)}
78+
{filteredTalks?.map((session) => (
79+
<TalkCard key={session.id} {...talkCardAdapter(session)} />
80+
))}
81+
</AnimatePresence>
82+
</StyledAgenda>
83+
<a
84+
href={ROUTE_SCHEDULE}
85+
style={{
86+
textDecoration: "none",
87+
fontWeight: "bold",
88+
margin: "0.5rem",
89+
}}
90+
>
91+
📅 Back to schedule
92+
</a>
93+
</StyledMain>
94+
);
95+
};
96+
97+
vi.mock("@hooks/useFetchTalks", () => ({
98+
...vi.importActual("@hooks/useFetchTalks"),
99+
useFetchLiveView: vi.fn(),
100+
}));
101+
102+
vi.mock("react-router-dom", () => {
103+
return {
104+
Link: ({ children, to, style }) => (
105+
<a href={to} style={style} data-testid="mock-link">
106+
{children}
107+
</a>
108+
),
109+
};
110+
});
111+
112+
// Mock the renderWithQueryClientAndRouter function to avoid using MemoryRouter
113+
vi.mock("../../utils/testing/testUtils", async (importOriginal) => {
114+
const actual = await importOriginal();
115+
return {
116+
...actual,
117+
renderWithQueryClientAndRouter: actual.renderWithQueryClient,
118+
};
119+
});
7120

8121
describe("Live view component", () => {
122+
const originalSetInterval = global.setInterval;
123+
const originalClearInterval = global.clearInterval;
124+
125+
let setIntervalMock: MockedFunction<typeof global.setInterval>;
126+
let clearIntervalMock: MockedFunction<typeof global.clearInterval>;
127+
128+
beforeEach(() => {
129+
setIntervalMock = vi.fn(() => {
130+
return 123;
131+
});
132+
clearIntervalMock = vi.fn();
133+
134+
global.setInterval =
135+
setIntervalMock as unknown as typeof global.setInterval;
136+
global.clearInterval =
137+
clearIntervalMock as unknown as typeof global.clearInterval;
138+
139+
vi.clearAllMocks();
140+
});
141+
142+
afterEach(() => {
143+
// Restore original implementations
144+
global.setInterval = originalSetInterval;
145+
global.clearInterval = originalClearInterval;
146+
});
147+
9148
it("renders without crashing", () => {
149+
vi.spyOn(useFetchTalksModule, "useFetchLiveView").mockReturnValue({
150+
isLoading: true,
151+
error: null,
152+
data: undefined,
153+
isError: false,
154+
isSuccess: false,
155+
isIdle: false,
156+
status: "loading",
157+
isFetching: true,
158+
refetch: vi.fn(),
159+
remove: vi.fn(),
160+
});
161+
10162
renderWithQueryClientAndRouter(<LiveView />);
11163
const titleElement = screen.getByText(/Live Schedule/);
12164
expect(titleElement).toBeInTheDocument();
13165
});
166+
167+
it("cleans up the interval on unmount", () => {
168+
vi.spyOn(useFetchTalksModule, "useFetchLiveView").mockReturnValue({
169+
isLoading: false,
170+
error: null,
171+
data: [],
172+
isError: false,
173+
isSuccess: true,
174+
isIdle: false,
175+
status: "success",
176+
isFetching: false,
177+
refetch: vi.fn(),
178+
remove: vi.fn(),
179+
});
180+
181+
const { unmount } = renderWithQueryClientAndRouter(<LiveView />);
182+
183+
unmount();
184+
185+
expect(clearIntervalMock).toHaveBeenCalledTimes(1);
186+
expect(clearIntervalMock).toHaveBeenCalledWith(123); // The mock interval ID
187+
});
14188
});

0 commit comments

Comments
 (0)