diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 85ce14c91..e63d5b752 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -2,25 +2,29 @@ name: sonar analysis on: push: - branches: [ "main" ] + branches: ["main"] pull_request: - types: [ opened, synchronize, reopened ] + types: [opened, synchronize, reopened] jobs: sonarcloud: name: SonarCloud runs-on: ubuntu-latest + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} steps: # Checkout - name: checkout uses: actions/checkout@v3 with: - fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis # Load cache modules - name: Cache node modules id: cache-npm uses: actions/cache@v3 + env: + cache-name: cache-node-modules with: path: ~/.npm key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} @@ -42,7 +46,7 @@ jobs: # Send report to sonar - name: SonarCloud Scan - uses: SonarSource/sonarcloud-github-action@master + uses: SonarSource/sonarqube-scan-action@v4.2.1 env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..fdaedb981 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "command": "npm start", + "name": "Run npm start", + "request": "launch", + "type": "node-terminal" + }, + ] +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8955a8a46..d6fcbe59e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6430,9 +6430,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001726", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz", - "integrity": "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==", + "version": "1.0.30001760", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz", + "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==", "funding": [ { "type": "opencollective", diff --git a/public/images/wtc-auditorio.jpg b/public/images/wtc-auditorio.jpg new file mode 100644 index 000000000..bfce8b4bd Binary files /dev/null and b/public/images/wtc-auditorio.jpg differ diff --git a/public/images/wtc.jpg b/public/images/wtc.jpg new file mode 100644 index 000000000..cbec6c8f7 Binary files /dev/null and b/public/images/wtc.jpg differ diff --git a/src/2023/Cfp/CfpSection2023.test.tsx b/src/2023/Cfp/CfpSection2023.test.tsx deleted file mode 100644 index 59cd39a0e..000000000 --- a/src/2023/Cfp/CfpSection2023.test.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { vi } from "vitest"; - -// Mock useWindowSize to control the window size in tests -vi.mock("react-use", () => ({ - useWindowSize: vi.fn(), -})); - -import React from "react"; -import { render, screen, waitFor } from "@testing-library/react"; -import "@testing-library/jest-dom"; -import CfpSection2023 from "./CfpSection2023"; -import { useWindowSize } from "react-use"; -import conferenceData from "../../data/2023.json"; -import { data } from "./CfpData"; - -describe("CfpSection2023", () => { - beforeEach(() => { - // Reset the mock before each test - useWindowSize.mockReset(); - useWindowSize.mockReturnValue({ width: 1024 }); // Default width - }); - - it("should render without crashing", () => { - render(); - }); - - it("should render the title and subtitle", () => { - render(); - expect( - screen.getByText("CFP Committee", { exact: false }), - ).toBeInTheDocument(); - expect( - screen.getByText( - "We're excited to announce the members of the Call for Papers committee for the next DevBcn conference! These experienced professionals will be reviewing and selecting the best talks and workshops for the upcoming event.", - ), - ).toBeInTheDocument(); - }); - - it("should render the tracks and members", () => { - render(); - data.forEach((track) => { - expect(screen.getAllByText(track.name, { exact: false })).not.toBeNull(); - track.members - .filter((member) => member.photo !== "") - .forEach((member) => { - expect( - screen.getAllByText(member.name, { exact: false }), - ).not.toBeNull(); - }); - }); - }); - - it("should render member photos", () => { - render(); - data.forEach((track) => { - track.members - .filter((member) => member.photo !== "") - .forEach((member) => { - const image = screen.getAllByAltText(member.name); - expect(image).not.toBeNull(); - expect(image.at(0)).toHaveAttribute("src", member.photo); - }); - }); - }); - - it("should render twitter links", () => { - render(); - data.forEach((track) => { - track.members - .filter((member) => member.twitter !== "") - .forEach((member) => { - const twitterLinks = screen.getAllByRole("link"); - const twitterLink = twitterLinks.find( - (link) => link.getAttribute("href") === member.twitter, - ); - expect(twitterLink).toBeInTheDocument(); - expect(twitterLink).toHaveAttribute("href", member.twitter); - }); - }); - }); - - it("should render linkedIn links", () => { - render(); - data.forEach((track) => { - track.members - .filter((member) => member.linkedIn !== "") - .forEach((member) => { - const linkedInLinks = screen.getAllByRole("link"); - const linkedInLink = linkedInLinks.find( - (link) => link.getAttribute("href") === member.linkedIn, - ); - expect(linkedInLink).toBeInTheDocument(); - expect(linkedInLink).toHaveAttribute("href", member.linkedIn); - }); - }); - }); - - it("should update the document title", async () => { - render(); - await waitFor(() => { - expect(document.title).toBe( - `CFP Committee — DevBcn - Barcelona Developers Conference — ${conferenceData.edition}`, - ); - }); - }); - - it("should not render the icons when the width is smaller than the breakpoint", () => { - (useWindowSize as jest.Mock).mockReturnValue({ width: 767 }); - render(); - const lessIcon = screen.queryByAltText("more than - icon"); - const moreIcon = screen.queryByAltText("Less than - icon"); - expect(lessIcon).not.toBeInTheDocument(); - expect(moreIcon).not.toBeInTheDocument(); - }); -}); diff --git a/src/2023/Cfp/CfpSection2023.tsx b/src/2023/Cfp/CfpSection2023.tsx deleted file mode 100644 index fa7e86b54..000000000 --- a/src/2023/Cfp/CfpSection2023.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import React, { FC } from "react"; -import { SectionWrapper } from "@components/SectionWrapper/SectionWrapper"; -import { Color } from "@styles/colors"; -import { - StyledLessIcon, - StyledMoreIcon, - StyledSpeakersSection, -} from "../Speakers/Speakers.style"; -import TitleSection from "@components/SectionTitle/TitleSection"; -import { MOBILE_BREAKPOINT } from "@constants/BreakPoints"; -import { useWindowSize } from "react-use"; -import TwitterIcon from "@components/Icons/Twitter"; -import LinkedinIcon from "@components/Icons/Linkedin"; - -import conferenceData from "@data/2023.json"; -import { CfpTrackProps, data } from "./CfpData"; -import { styled } from "styled-components"; -import { - StyledAboutImage, - StyledSocialIconsWrapper, -} from "@views/About/components/Style.AboutCard"; -import { useDocumentTitleUpdater } from "@hooks/useDocumentTitleUpdate"; - -const TrackName = styled.h2` - padding-top: 1.2rem; - padding-bottom: 0.8rem; - font-size: 1.5rem; - color: ${Color.DARK_BLUE}; -`; - -const MemberName = styled.h5` - font-size: 0.8rem; - color: ${Color.DARK_BLUE}; - text-align: left; -`; - -const CfpTrackComponent: FC> = ({ - track, -}) => ( - <> -
- {track.name} -
-
- {track.members.map((member) => { - return ( -
- {member.photo !== "" && ( -
- - {member.name} - - {member.twitter !== "" && ( - - )} - {member.linkedIn !== "" && ( - - )} - -
- )} -
- ); - })} -
- -); - -const CfpSection2023: FC> = () => { - const { width } = useWindowSize(); - - useDocumentTitleUpdater("CFP Committee", conferenceData.edition); - return ( - <> - - - - {width > MOBILE_BREAKPOINT && ( - <> - - - - )} - - {data.map((track) => ( - - ))} - -
 
- - ); -}; - -export default CfpSection2023; diff --git a/src/2023/Cfp/CfpSectionWrapper.tsx b/src/2023/Cfp/CfpSectionWrapper.tsx new file mode 100644 index 000000000..79d870851 --- /dev/null +++ b/src/2023/Cfp/CfpSectionWrapper.tsx @@ -0,0 +1,10 @@ +import React, { FC } from "react"; +import CfpSection from "@views/Cfp/CfpSection"; +import data2023 from "@data/2023.json"; +import { data } from "./CfpData"; + +export const CfpSectionWrapper: FC = () => { + return ; +}; + +export default CfpSectionWrapper; diff --git a/src/2023/Home/components/ActionButtons/ActionButtons.tsx b/src/2023/Home/components/ActionButtons/ActionButtons.tsx deleted file mode 100644 index 0986c80ba..000000000 --- a/src/2023/Home/components/ActionButtons/ActionButtons.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React, { FC, useCallback } from "react"; -import data from "../../../../data/2023.json"; -import Button from "../../../../components/UI/Button"; -import { styled } from "styled-components"; -import { BIG_BREAKPOINT } from "../../../../constants/BreakPoints"; -import { gaEventTracker } from "../../../../components/analytics/Analytics"; -import { useDateInterval } from "../../../../hooks/useDateInterval"; -import { useUrlBuilder } from "../../../../services/urlBuilder"; - -const StyledActionDiv = styled.div` - display: flex; - text-align: center; - - @media (max-width: ${BIG_BREAKPOINT}px) { - flex-direction: column; - width: 75%; - } -`; - -const ActionButtons: FC> = () => { - const { isTicketsDisabled, isSponsorDisabled, isCfpDisabled } = - useDateInterval(new Date(), data); - - const trackSponsorshipInfo = useCallback(() => { - gaEventTracker("sponsorship", "sponsorship"); - }, []); - - const trackTickets = useCallback(() => { - gaEventTracker("ticket", "tickets"); - }, []); - - const trackCFP = useCallback(() => { - gaEventTracker("CFP", "CFP"); - }, []); - - return ( - - + ), +})); + +// Mock useWindowSize +vi.mock("react-use", () => ({ + useWindowSize: () => ({ width: 1200, height: 800 }), +})); + +const mockMeeting: IMeeting = { + id: 12345, + title: "Test Talk Title", + description: "This is a test talk description with important content.", + videoUrl: "https://www.youtube.com/embed/test123", + slidesURL: "https://slides.example.com/test", + videoTags: ["React", "TypeScript", "Testing"], + speakers: [ + { id: "speaker-1", name: "John Doe" }, + { id: "speaker-2", name: "Jane Smith" }, + ], + level: "Intermediate", + type: "Talk", + language: "English", + track: "Frontend", + startDate: "2024-06-17", + endDate: "2024-06-17", + startTime: "10:00", + endTime: "11:00", +}; + +const mockSpeakers: ISpeaker[] = [ + { + id: "speaker-1", + fullName: "John Doe", + speakerImage: "/images/speakers/john.jpg", + bio: "Test bio", + tagLine: "Test tagline", + sessions: [], + links: [], + }, + { + id: "speaker-2", + fullName: "Jane Smith", + speakerImage: "/images/speakers/jane.jpg", + bio: "Test bio 2", + tagLine: "Test tagline 2", + sessions: [], + links: [], + }, +]; + +const renderMeetingDetail = ( + meeting: Partial = {}, + speakers: ISpeaker[] = mockSpeakers +) => { + return render( + + + + ); +}; + +describe("MeetingDetail", () => { + it("renders the meeting title", () => { + renderMeetingDetail(); + expect(screen.getByText(/Test Talk Title/)).toBeInTheDocument(); + }); + + it("renders the meeting description", () => { + renderMeetingDetail(); + expect( + screen.getByText(/This is a test talk description/) + ).toBeInTheDocument(); + }); + + it("renders speaker names with links", () => { + renderMeetingDetail(); + expect(screen.getByText("John Doe")).toBeInTheDocument(); + expect(screen.getByText("Jane Smith")).toBeInTheDocument(); + }); + + it("renders speaker images", () => { + renderMeetingDetail(); + const speakerImages = screen.getAllByRole("img", { name: /John Doe|Jane Smith/ }); + expect(speakerImages).toHaveLength(2); + }); + + it("renders vote talk link with correct href", () => { + renderMeetingDetail(); + const voteLink = screen.getByText(/Vote this talk/).closest("a"); + expect(voteLink).toHaveAttribute( + "href", + "https://openfeedback.io/test-feedback-id/0/12345" + ); + }); + + it("renders video iframe when videoUrl is provided", () => { + renderMeetingDetail(); + const iframe = screen.getByTitle("Test Talk Title"); + expect(iframe).toBeInTheDocument(); + expect(iframe).toHaveAttribute( + "src", + "https://www.youtube.com/embed/test123" + ); + }); + + it("does not render video iframe when videoUrl is empty", () => { + renderMeetingDetail({ videoUrl: undefined }); + expect(screen.queryByTitle("Test Talk Title")).not.toBeInTheDocument(); + }); + + it("renders slides link when slidesURL is provided", () => { + renderMeetingDetail(); + const slidesLink = screen.getByText(/Session Slides/).closest("a"); + expect(slidesLink).toHaveAttribute( + "href", + "https://slides.example.com/test" + ); + }); + + it("does not render slides link when slidesURL is empty", () => { + renderMeetingDetail({ slidesURL: "" }); + expect(screen.queryByText(/Session Slides/)).not.toBeInTheDocument(); + }); + + it("renders video tags", () => { + renderMeetingDetail(); + expect(screen.getByText("React")).toBeInTheDocument(); + expect(screen.getByText("TypeScript")).toBeInTheDocument(); + expect(screen.getByText("Testing")).toBeInTheDocument(); + }); + + it("renders track information", () => { + renderMeetingDetail(); + expect(screen.getByText(/Track:/)).toBeInTheDocument(); + expect(screen.getByText(/Frontend/)).toBeInTheDocument(); + }); + + it("renders level and type information", () => { + renderMeetingDetail(); + expect(screen.getByText(/Talk Intermediate/)).toBeInTheDocument(); + }); + + it("renders go back link", () => { + renderMeetingDetail(); + const backLink = screen.getByText("Go back"); + expect(backLink).toBeInTheDocument(); + expect(backLink.closest("a")).toHaveAttribute("href", "/talks"); + }); + + it("renders add to calendar button", () => { + renderMeetingDetail(); + expect(screen.getByTestId("add-to-calendar")).toBeInTheDocument(); + }); +}); diff --git a/src/views/MeetingDetail/MeetingDetail.tsx b/src/views/MeetingDetail/MeetingDetail.tsx index d5ed62d99..69be9c800 100644 --- a/src/views/MeetingDetail/MeetingDetail.tsx +++ b/src/views/MeetingDetail/MeetingDetail.tsx @@ -103,10 +103,13 @@ const MeetingDetail: FC> = ({ meeting, speakers: mySpeakers, openFeedbackId, + edition = conferenceData.edition, + speakerDetailRoute = ROUTE_SPEAKER_DETAIL, + talksRoute = ROUTE_TALKS, }) => { const { width } = useWindowSize(); - useDocumentTitleUpdater(meeting.title, conferenceData.edition); + useDocumentTitleUpdater(meeting.title, edition); const finalMeetingInfo: MyType = { ...meeting, @@ -242,7 +245,7 @@ const MeetingDetail: FC> = ({ /> - + {speaker.fullName} @@ -254,7 +257,7 @@ const MeetingDetail: FC> = ({
> = ({ speaker, + edition = conferenceData.edition, + speakersRoute = ROUTE_SPEAKERS, + talkDetailRoute = ROUTE_TALK_DETAIL, }) => { const { width } = useWindowSize(); useEffect(() => { - document.title = `${speaker.fullName} — ${conferenceData.title} — ${conferenceData.edition}`; - }, [speaker.fullName]); + document.title = `${speaker.fullName} — ${conferenceData.title} — ${edition}`; + }, [speaker.fullName, edition]); const hasSessions = (): boolean => (speaker.sessions && speaker.sessions.length > 0) || false; @@ -124,7 +130,7 @@ const SpeakerDetail: FC> = ({ {speaker?.sessions?.map((session) => (
  • > = ({ )} ({ captureException: vi.fn(), })); -// Mock the 2025.json data -vi.mock("../../data/2025.json", () => { +// Mock the 2026.json data +vi.mock("../../data/2026.json", () => { const mockData = { hideSpeakers: false, - edition: "2024", + edition: "2026", title: "DevBcn", cfp: { - startDay: "2023-01-01T00:00:00", - endDay: "2023-02-01T00:00:00", + startDay: "2026-01-01T00:00:00", + endDay: "2026-03-01T00:00:00", link: "https://example.com/cfp", }, }; @@ -94,7 +94,7 @@ describe("Speakers component", () => { // Mock Date to return a date within the CFP period const originalDate = Date; - const mockDate = new Date("2023-01-15"); + const mockDate = new Date("2026-01-15"); // This ensures that both new Date() and Date.now() use our mock date global.Date = class extends Date { @@ -130,7 +130,7 @@ describe("Speakers component", () => { // Mock Date to return a date within the CFP period const originalDate = Date; - const mockDate = new Date("2023-01-15"); + const mockDate = new Date("2026-01-15"); // This ensures that both new Date() and Date.now() use our mock date global.Date = class extends Date { diff --git a/src/views/Speakers/Speakers.tsx b/src/views/Speakers/Speakers.tsx index 606fbd0a6..9f4b05240 100644 --- a/src/views/Speakers/Speakers.tsx +++ b/src/views/Speakers/Speakers.tsx @@ -15,7 +15,7 @@ import { StyledSpeakersSection, StyledWaveContainer, } from "./Speakers.style"; -import webData from "@data/2025.json"; +import webData from "@data/2026.json"; import Button from "@components/UI/Button"; import { gaEventTracker } from "@components/analytics/Analytics"; import { useFetchSpeakers } from "@hooks/useFetchSpeakers"; @@ -29,13 +29,19 @@ const LessThanGreaterThan = () => ( ); -const Speakers: FC> = () => { +interface SpeakersProps { + conferenceConfig?: typeof webData; +} + +const Speakers: FC> = ({ + conferenceConfig = webData, +}) => { const { width } = useWindowSize(); const today = new Date(); const isBetween = (startDay: Date, endDay: Date): boolean => startDay < new Date() && endDay > today; - const { error, data, isLoading } = useFetchSpeakers(); + const { error, data, isLoading } = useFetchSpeakers(conferenceConfig.edition); useSentryErrorReport(error); @@ -44,11 +50,11 @@ const Speakers: FC> = () => { }, []); useEffect(() => { - document.title = `Speakers — ${webData.title} — ${webData.edition}`; + document.title = `Speakers — ${conferenceConfig.title} — ${conferenceConfig.edition}`; }); - const CFPStartDay = new Date(webData.cfp.startDay); - const CFPEndDay = new Date(webData.cfp.endDay); + const CFPStartDay = new Date(conferenceConfig.cfp.startDay); + const CFPEndDay = new Date(conferenceConfig.cfp.endDay); return ( <> @@ -75,11 +81,11 @@ const Speakers: FC> = () => {
  • )} - {webData.hideSpeakers ? ( + {conferenceConfig.hideSpeakers ? (

    No selected speakers yet. Keep in touch in our social media for upcoming announcements @@ -89,7 +95,7 @@ const Speakers: FC> = () => { )) )} diff --git a/src/views/Talks/LiveView.test.tsx b/src/views/Talks/LiveView.test.tsx index 64eea81de..ba68a5090 100644 --- a/src/views/Talks/LiveView.test.tsx +++ b/src/views/Talks/LiveView.test.tsx @@ -3,8 +3,8 @@ import * as useFetchTalksModule from "@hooks/useFetchTalks"; import { useFetchLiveView } from "@hooks/useFetchTalks"; import { Loading } from "@components/Loading/Loading"; import { UngroupedSession } from "./liveView.types"; -import conference from "@data/2025.json"; -import { TalkCard } from "./components/TalkCard"; +import conference from "@data/2026.json"; +import { TalkCard } from "@components/common/TalkCard"; import { StyledAgenda, StyledMain } from "./Talks.style"; import { talkCardAdapter } from "./TalkCardAdapter"; import { useSentryErrorReport } from "@hooks/useSentryErrorReport"; diff --git a/src/views/Talks/LiveView.tsx b/src/views/Talks/LiveView.tsx index b4da5ab7a..8a5d8fbe5 100644 --- a/src/views/Talks/LiveView.tsx +++ b/src/views/Talks/LiveView.tsx @@ -3,7 +3,7 @@ import { useFetchLiveView } from "@hooks/useFetchTalks"; import { Loading } from "@components/Loading/Loading"; import { UngroupedSession } from "./liveView.types"; import conference from "@data/2025.json"; -import { TalkCard } from "./components/TalkCard"; +import { TalkCard } from "@components/common/TalkCard"; import { StyledAgenda, StyledMain } from "./Talks.style"; import { talkCardAdapter } from "./TalkCardAdapter"; import { useSentryErrorReport } from "@hooks/useSentryErrorReport"; diff --git a/src/views/Talks/TalkCardAdapter.ts b/src/views/Talks/TalkCardAdapter.ts index fa3b64ec4..22fba67ca 100644 --- a/src/views/Talks/TalkCardAdapter.ts +++ b/src/views/Talks/TalkCardAdapter.ts @@ -1,5 +1,5 @@ import { UngroupedSession } from "./liveView.types"; -import { TalkCardProps } from "./components/TalkCard"; +import { TalkCardProps } from "@components/common/TalkCard"; import { QuestionAnswers, diff --git a/src/views/Talks/Talks.test.tsx b/src/views/Talks/Talks.test.tsx index 43c14857c..3019c2026 100644 --- a/src/views/Talks/Talks.test.tsx +++ b/src/views/Talks/Talks.test.tsx @@ -7,7 +7,11 @@ import { } from "../../utils/testing/testUtils"; import { ROUTE_MEETING_DETAIL_PLAIN } from "@constants/routes"; import { useFetchTalks } from "@hooks/useFetchTalks"; -import { IGroup } from "@/types/sessions"; +import { + IGroup, + TopRatedTalk, + TopTalkWithSpeaker, +} from "@/types/sessions"; import userEvent from "@testing-library/user-event"; import { vi } from "vitest"; @@ -37,6 +41,99 @@ vi.mock("../../utils/testing/testUtils", async (importOriginal) => { }; }); +const mockTopTenTalks: TopRatedTalk[] = [ + { + id: "df057475-0b6a-4fab-8e0d-c5576230dd5c", + speaker: "Victor Rentea", + talk: "Top 10 Rest API Design Falls", + link: ROUTE_MEETING_DETAIL_PLAIN.replace(":id", "838798"), + }, + { + id: "d32cdd87-3c7d-47bb-98ec-b255d1e4b9ba", + speaker: "Laura Perea", + talk: "GenAI among us", + link: ROUTE_MEETING_DETAIL_PLAIN.replace(":id", "945091"), + }, + { + id: "eb3852c1-acf8-42a6-988d-365fad2a5668", + speaker: "Brian Vermeer", + talk: "Don't Get Burned! Secure Coding Essentials in Java to protect your application", + link: ROUTE_MEETING_DETAIL_PLAIN.replace(":id", "851481"), + }, + { + id: "625b53c9-edea-4e47-a5ba-2ee661c539e3", + speaker: "Álvaro Sánchez-Mariscal", + talk: "Revealing the magic behind Java annotations", + link: ROUTE_MEETING_DETAIL_PLAIN.replace(":id", "843845"), + }, + { + id: "7b1c534c-39a5-4398-93e5-626010f00198", + speaker: "Alexander Chatzizacharias", + talk: "What is multimodal RAG, and can we build a village with it?", + link: ROUTE_MEETING_DETAIL_PLAIN.replace(":id", "832774"), + }, + { + id: "ebab2b92-503f-4baa-b3ab-064865853223", + speaker: "Bert Jan Schrijver", + talk: "Generic or Specific? Making sensible software design decisions", + link: ROUTE_MEETING_DETAIL_PLAIN.replace(":id", "827688"), + }, + { + id: "11554c51-dc18-407b-b7b4-b8ad2f925b2a", + speaker: "Marc Nuri", + talk: "Model Context Protocol Servers 101: Unlocking the Power of AI", + link: ROUTE_MEETING_DETAIL_PLAIN.replace(":id", "874255"), + }, + { + id: "10937eaf-a0da-48c9-82d6-8711ca26fb16", + speaker: "Andres Almiray", + talk: "Maven Productivity Tips", + link: ROUTE_MEETING_DETAIL_PLAIN.replace(":id", "860854"), + }, + { + id: "5ce27637-12b4-4dfe-830d-166d88c837ad", + speaker: "Milen Dyankov", + talk: "AI for Java Developers - From Buzzword to Code", + link: ROUTE_MEETING_DETAIL_PLAIN.replace(":id", "873844"), + }, + { + id: "2aea7252-6822-4f42-a9d4-fa830f29df40", + speaker: "Rijo Sam", + talk: "Java Beyond Frameworks: Avoiding Lock-In with Agnostic Design", + link: ROUTE_MEETING_DETAIL_PLAIN.replace(":id", "875233"), + }, +]; + +const mockTopThreeTalks: TopTalkWithSpeaker[] = [ + { + id: "df057475-0b6a-4fab-8e0d-c5576230dd5c", + award: "Funniest talk", + speaker: "Victor Rentea", + speakerImage: + "https://sessionize.com/image/2fde-400o400o1-NVbZAJzrFZpcRjEe5khxjo.png", + talk: "Top 10 Rest API Design Falls", + link: ROUTE_MEETING_DETAIL_PLAIN.replace(":id", "838798"), + }, + { + id: "d32cdd87-3c7d-47bb-98ec-b255d1e4b9ba", + speaker: "Laura Perea", + award: "Best Rated", + speakerImage: + "https://sessionize.com/image/8df6-400o400o1-LKJE9Ej5xvBK92FtxJDo6U.png", + talk: "GenAI among us", + link: ROUTE_MEETING_DETAIL_PLAIN.replace(":id", "945091"), + }, + { + id: "11554c51-dc18-407b-b7b4-b8ad2f925b2a", + speaker: "Marc Nuri", + award: "Most original", + speakerImage: + "https://sessionize.com/image/3a9a-400o400o1-sJBQfR5Ki5BGPEDG8GQgKM.jpg", + talk: "Model Context Protocol Servers 101: Unlocking the Power of AI", + link: ROUTE_MEETING_DETAIL_PLAIN.replace(":id", "874255"), + }, +]; + describe("Talks", () => { beforeEach(() => { // Reset all mocks before each test @@ -106,7 +203,12 @@ describe("Talks", () => { // Tests for the topThreeTalks array it("renders the top three talks section with correct awards", () => { - renderWithQueryClient(); + renderWithQueryClient( + , + ); // Check for award titles expect(screen.getByText("Funniest talk")).toBeInTheDocument(); @@ -115,7 +217,12 @@ describe("Talks", () => { }); it("renders all top three talks with correct speaker names and talk titles", () => { - renderWithQueryClient(); + renderWithQueryClient( + , + ); // Check for speaker names expect(screen.getByText("Victor Rentea")).toBeInTheDocument(); @@ -135,7 +242,12 @@ describe("Talks", () => { }); it("renders top three talks with correct images", () => { - renderWithQueryClient(); + renderWithQueryClient( + , + ); // Check for images with correct src attributes const images = screen.getAllByRole("img"); @@ -173,7 +285,12 @@ describe("Talks", () => { }); it("renders top three talks with correct links", () => { - renderWithQueryClient(); + renderWithQueryClient( + , + ); // Check that links are correctly formatted const victorLink = screen.getByText("Victor Rentea").closest("a"); @@ -196,12 +313,22 @@ describe("Talks", () => { // Tests for the topTenTalks array it("renders the top ten talks section", () => { - renderWithQueryClient(); + renderWithQueryClient( + , + ); expect(screen.getByText("🔝 Top Ten rated talks")).toBeInTheDocument(); }); it("renders all top ten talks with correct links", () => { - renderWithQueryClient(); + renderWithQueryClient( + , + ); // Check for specific talks expect( diff --git a/src/views/Talks/Talks.tsx b/src/views/Talks/Talks.tsx index 143145de6..db0f9c04c 100644 --- a/src/views/Talks/Talks.tsx +++ b/src/views/Talks/Talks.tsx @@ -17,30 +17,27 @@ import { SelectButton, SelectButtonChangeEvent } from "primereact/selectbutton"; import "primereact/resources/themes/lara-light-indigo/theme.css"; import "@styles/theme.css"; import { useSentryErrorReport } from "@hooks/useSentryErrorReport"; -import { ROUTE_MEETING_DETAIL_PLAIN } from "@constants/routes"; - -interface TrackInfo { - name: string; - code?: string; -} - -interface TopRatedTalk { - id: string; - speaker: string; - talk: string; - link: string; -} +import { + TopRatedTalk, + TopTalkWithSpeaker, + TrackInfo, +} from "@/types/sessions"; -interface TopTalkWithSpeaker extends TopRatedTalk { - speakerImage: string; - award: string; +interface TalksProps { + conferenceConfig?: typeof conferenceData; + topTenTalks?: Array; + topThreeTalks?: Array; } -const Talks: FC> = () => { +const Talks: FC> = ({ + conferenceConfig = conferenceData, + topTenTalks = [], + topThreeTalks = [], +}) => { const [selectedGroupId, setSelectedGroupId] = useState( null, ); - const { isLoading, error, data } = useFetchTalks(); + const { isLoading, error, data } = useFetchTalks(conferenceConfig.edition); useEffect(() => { const sessionSelectedGroupCode = @@ -48,7 +45,7 @@ const Talks: FC> = () => { const sessionSelectedGroupName = sessionStorage.getItem("selectedGroupName"); - document.title = `Talks - ${conferenceData.title} - ${conferenceData.edition}`; + document.title = `Talks - ${conferenceConfig.title} - ${conferenceConfig.edition}`; if (sessionSelectedGroupCode && sessionSelectedGroupName) { setSelectedGroupId({ @@ -56,7 +53,7 @@ const Talks: FC> = () => { code: sessionSelectedGroupCode, }); } - }, []); + }, [conferenceConfig.title, conferenceConfig.edition]); useSentryErrorReport(error); @@ -68,106 +65,15 @@ const Talks: FC> = () => { { name: "All Tracks", code: undefined }, ...(data !== undefined ? data - .flatMap((group) => ({ - code: group?.groupId?.toString(), - name: removeParenthesesContent(group.groupName), - })) - .sort((a, b) => a.name.localeCompare(b.name)) + .flatMap((group) => ({ + code: group?.groupId?.toString(), + name: removeParenthesesContent(group.groupName), + })) + .sort((a, b) => a.name.localeCompare(b.name)) : []), ]; - const topTenTalks: Array = [ - { - id: "df057475-0b6a-4fab-8e0d-c5576230dd5c", - speaker: "Victor Rentea", - talk: "Top 10 Rest API Design Falls", - link: ROUTE_MEETING_DETAIL_PLAIN.replace(":id", "838798"), - }, - { - id: "d32cdd87-3c7d-47bb-98ec-b255d1e4b9ba", - speaker: "Laura Perea", - talk: "GenAI among us", - link: ROUTE_MEETING_DETAIL_PLAIN.replace(":id", "945091"), - }, - { - id: "eb3852c1-acf8-42a6-988d-365fad2a5668", - speaker: "Brian Vermeer", - talk: "Don't Get Burned! Secure Coding Essentials in Java to protect your application", - link: ROUTE_MEETING_DETAIL_PLAIN.replace(":id", "851481"), - }, - { - id: "625b53c9-edea-4e47-a5ba-2ee661c539e3", - speaker: "Álvaro Sánchez-Mariscal", - talk: "Revealing the magic behind Java annotations", - link: ROUTE_MEETING_DETAIL_PLAIN.replace(":id", "843845"), - }, - { - id: "7b1c534c-39a5-4398-93e5-626010f00198", - speaker: "Alexander Chatzizacharias", - talk: "What is multimodal RAG, and can we build a village with it?", - link: ROUTE_MEETING_DETAIL_PLAIN.replace(":id", "832774"), - }, - { - id: "ebab2b92-503f-4baa-b3ab-064865853223", - speaker: "Bert Jan Schrijver", - talk: "Generic or Specific? Making sensible software design decisions", - link: ROUTE_MEETING_DETAIL_PLAIN.replace(":id", "827688"), - }, - { - id: "11554c51-dc18-407b-b7b4-b8ad2f925b2a", - speaker: "Marc Nuri", - talk: "Model Context Protocol Servers 101: Unlocking the Power of AI", - link: ROUTE_MEETING_DETAIL_PLAIN.replace(":id", "874255"), - }, - { - id: "10937eaf-a0da-48c9-82d6-8711ca26fb16", - speaker: "Andres Almiray", - talk: "Maven Productivity Tips", - link: ROUTE_MEETING_DETAIL_PLAIN.replace(":id", "860854"), - }, - { - id: "5ce27637-12b4-4dfe-830d-166d88c837ad", - speaker: "Milen Dyankov", - talk: "AI for Java Developers - From Buzzword to Code", - link: ROUTE_MEETING_DETAIL_PLAIN.replace(":id", "873844"), - }, - { - id: "2aea7252-6822-4f42-a9d4-fa830f29df40", - speaker: "Rijo Sam", - talk: "Java Beyond Frameworks: Avoiding Lock-In with Agnostic Design", - link: ROUTE_MEETING_DETAIL_PLAIN.replace(":id", "875233"), - }, - ]; - const topThreeTalks: Array = [ - { - id: "df057475-0b6a-4fab-8e0d-c5576230dd5c", - award: "Funniest talk", - speaker: "Victor Rentea", - speakerImage: - "https://sessionize.com/image/2fde-400o400o1-NVbZAJzrFZpcRjEe5khxjo.png", - talk: "Top 10 Rest API Design Falls", - link: ROUTE_MEETING_DETAIL_PLAIN.replace(":id", "838798"), - }, - { - id: "d32cdd87-3c7d-47bb-98ec-b255d1e4b9ba", - speaker: "Laura Perea", - award: "Best Rated", - speakerImage: - "https://sessionize.com/image/8df6-400o400o1-LKJE9Ej5xvBK92FtxJDo6U.png", - talk: "GenAI among us", - link: ROUTE_MEETING_DETAIL_PLAIN.replace(":id", "945091"), - }, - { - id: "11554c51-dc18-407b-b7b4-b8ad2f925b2a", - speaker: "Marc Nuri", - award: "Most original", - speakerImage: - "https://sessionize.com/image/3a9a-400o400o1-sJBQfR5Ki5BGPEDG8GQgKM.jpg", - talk: "Model Context Protocol Servers 101: Unlocking the Power of AI", - link: ROUTE_MEETING_DETAIL_PLAIN.replace(":id", "874255"), - }, - ]; const filteredTalks = selectedGroupId?.code ? data?.filter((talk) => talk.groupId.toString() === selectedGroupId.code) @@ -267,7 +173,7 @@ const Talks: FC> = () => {

    {isLoading &&

    Loading

    } - {conferenceData.hideTalks ? ( + {conferenceConfig.hideTalks ? (

    No talks selected yet. Keep in tap in our social media for upcoming announcements @@ -325,8 +231,8 @@ const Talks: FC> = () => { ))} diff --git a/src/views/Talks/components/TalkCard.tsx b/src/views/Talks/components/TalkCard.tsx deleted file mode 100644 index 9b80d8a08..000000000 --- a/src/views/Talks/components/TalkCard.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from "react"; -import { - TalkCard as CommonTalkCard, - TalkCardProps, -} from "@components/common/TalkCard"; - -export type { TalkCardProps }; - -export const TalkCard: React.FC> = ( - props, -) => { - return ; -}; diff --git a/src/views/Travel/Travel.tsx b/src/views/Travel/Travel.tsx index ee30b01b5..8dbf64729 100644 --- a/src/views/Travel/Travel.tsx +++ b/src/views/Travel/Travel.tsx @@ -1,12 +1,13 @@ -import React, { FC } from "react"; +import { FC } from "react"; +import { useParams } from "react-router"; import { Venue } from "./Venue"; import { ToBarcelona } from "./ToBarcelona"; -import data from "@data/2024.json"; import { StyledWaveContainer } from "../Speakers/Speakers.style"; import { styled } from "styled-components"; import { Color } from "@styles/colors"; import { Accommodation } from "./Accommodation"; import { useDocumentTitleUpdater } from "@hooks/useDocumentTitleUpdate"; +import { VenueWTC } from "./VenueWTC"; const StyledTravel = styled.div` max-width: 85rem; @@ -33,11 +34,14 @@ const StyledTravel = styled.div` `; const Travel: FC> = () => { - useDocumentTitleUpdater("Travel", data.edition); + const { id } = useParams<{ id: string }>(); + const edition = id || "2026"; + + useDocumentTitleUpdater("Travel", edition); return (

    - + {edition === "2026" ? : }
    diff --git a/src/views/Travel/VenueWTC.tsx b/src/views/Travel/VenueWTC.tsx new file mode 100644 index 000000000..0ac2b1653 --- /dev/null +++ b/src/views/Travel/VenueWTC.tsx @@ -0,0 +1,157 @@ +import React, { FC, Suspense } from "react"; +import "./map.css"; +import { styled } from "styled-components"; +import TitleSection from "@components/SectionTitle/TitleSection"; +import { Color } from "@styles/colors"; +import { + BIG_BREAKPOINT, + MAX_WIDTH, + MOBILE_BREAKPOINT, +} from "@constants/BreakPoints"; +import { useWindowSize } from "react-use"; +import { StyledLoadingImage } from "@components/Loading/Loading"; + +const StyledVenue = styled.div` + padding: 0.5rem 2rem 0.5rem; + text-align: left; + max-width: ${MAX_WIDTH}px; + margin: 0 auto; + + @media (max-width: ${BIG_BREAKPOINT}px) { + padding: 100px 1rem 50px; + } + + .image { + img.venue { + width: 95%; + margin: 0 2.5%; + text-align: center; + } + + a, + p { + padding-left: 10px; + } + + a { + text-decoration: none; + color: ${Color.DARK_BLUE}; + font-weight: bold; + } + } + + section.venue { + display: flex; + @media (max-width: ${BIG_BREAKPOINT}px) { + flex-direction: column; + } + } + + h4 { + margin: 15px 0 3px 10px; + } + + .image, + .map { + width: 50%; + @media (max-width: ${BIG_BREAKPOINT}px) { + width: 100%; + } + } +`; + +const StyledTrainLine = styled.span` + background-color: #00f200; + font-weight: bold; + padding: 1px 2px; + font-family: sans-serif; + font-size: 12px; +`; +export const StyledLessIcon = styled.img` + position: absolute; + left: -1rem; + top: 5rem; + height: 5rem; + + @media (min-width: ${BIG_BREAKPOINT}px) { + height: 10rem; + } +`; +export const StyledMoreIcon = styled.img` + position: absolute; + right: -1rem; + top: 5rem; + height: 5rem; + + @media (min-width: ${BIG_BREAKPOINT}px) { + height: 10rem; + } +`; + +export const VenueWTC: FC> = () => { + const { width } = useWindowSize(); + return ( + + + {width > MOBILE_BREAKPOINT && ( + <> + + + + )} +
    +
    + }> + World Trade Center, Barcelona + + + World Trade Center, Barcelona + +

    + 1ª planta Edif. Este, Moll de Barcelona, s/n, 08039 Barcelona +

    +

    Access by public transportation

    +

    🚇 Metro: Líneas L3: Parada Drassanes, Línea L2: Parada Paral·lel.

    +

    + 🚍 Bus: Línea V11, parada Moll de Barcelona. +

    +

    + 🚙 Access by car: via C-31 & B-10(14 minutes from the + Airport) +

    +

    Paid parking options

    +

    + + World Trade Center, Barcelona + +

    +
    +
    + Venue entrance +
    +
    +
    + ); +}; diff --git a/src/views/Workshops/Workshops.tsx b/src/views/Workshops/Workshops.tsx index 06069418c..39dce348e 100644 --- a/src/views/Workshops/Workshops.tsx +++ b/src/views/Workshops/Workshops.tsx @@ -1,7 +1,7 @@ import React, { FC, useEffect } from "react"; import { SectionWrapper } from "@components/SectionWrapper/SectionWrapper"; import { useFetchTalks } from "@hooks/useFetchTalks"; -import { TalkCard } from "../Talks/components/TalkCard"; +import { TalkCard } from "@components/common/TalkCard"; import conferenceData from "@data/2025.json"; import { styled } from "styled-components"; import { BIG_BREAKPOINT } from "@constants/BreakPoints"; diff --git a/src/views/sponsorship/Sponsorship.tsx b/src/views/sponsorship/Sponsorship.tsx index 18b0bc852..4c3bb8411 100644 --- a/src/views/sponsorship/Sponsorship.tsx +++ b/src/views/sponsorship/Sponsorship.tsx @@ -11,7 +11,6 @@ import { StyledSpeakersSection, } from "../Speakers/Speakers.style"; import { StyledMarginBottom } from "../Talks/Talks.style"; -import data from "@data/2025.json"; import { format } from "date-fns"; import Flicking from "@egjs/react-flicking"; import { AutoPlay } from "@egjs/flicking-plugins"; @@ -21,6 +20,18 @@ import { gaEventTracker } from "@components/analytics/Analytics"; import { useDocumentTitleUpdater } from "@hooks/useDocumentTitleUpdate"; // @ts-expect-error some quirky import import { AnimatePresence, motion } from "framer-motion"; +import { useParams } from "react-router"; +import data2023 from "@data/2023.json"; +import data2024 from "@data/2024.json"; +import data2025 from "@data/2025.json"; +import data2026 from "@data/2026.json"; + +const editions: Record = { + "2023": data2023, + "2024": data2024, + "2025": data2025, + "2026": data2026, +}; const StyledWaveContainer = styled.div` background: ${Color.DARK_BLUE}; @@ -92,6 +103,9 @@ const StyleMoreIcon = styled.img` `; const Sponsorship: FC> = () => { + const { id } = useParams<{ id: string }>(); + const edition = id || "2026"; + const data = editions[edition] || editions["2026"]; const { width } = useWindowSize(); const plugins = [ new AutoPlay({ duration: 2000, direction: "NEXT", stopOnHover: false }), @@ -137,7 +151,7 @@ const Sponsorship: FC> = () => { subtitle={`The DevBcn is the yearly event organised by Conferencia DevBcn S.L. Conference Talks will held on ${format( new Date(data.startDay), "MMMM do, yyyy", - )} at La Farga, Hospitalet de Llobregat`} + )} at ${data.venue}`} color={Color.DARK_BLUE} /> @@ -275,7 +289,7 @@ const Sponsorship: FC> = () => { {format(new Date(data.startDay), "MMMM do")} — {" ".concat(format(data.endDay, "do"))} {" "} - at the iconic La Farga, Hospitalet de Llobregat. This year, + at the iconic {data.venue}. This year, we're diving deep into the realms of Java, JVM, Cloud, DevOps, Frontend technologies, Leadership strategies, and groundbreaking advancements in Big Data and AI. @@ -294,7 +308,7 @@ const Sponsorship: FC> = () => { than ever. Curious? Access our{" "} @@ -310,7 +324,7 @@ const Sponsorship: FC> = () => { >