diff --git a/package-lock.json b/package-lock.json index 3ce75b2..a6dc73e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -439,26 +439,26 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.7.tgz", - "integrity": "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", + "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.7" + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.10" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.8.tgz", - "integrity": "sha512-TZIQ25pkSoaKEYYaHbbxkfL36GNsQ6iFiBbeuzAkLnXayKR1yP1zFe+NxuZWWsUyvt8icPU9CCq0sgWGXR1GEw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", + "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", "license": "MIT", "dependencies": { - "@babel/types": "^7.26.8" + "@babel/types": "^7.26.10" }, "bin": { "parser": "bin/babel-parser.js" @@ -1951,9 +1951,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz", - "integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", "license": "MIT", "dependencies": { "regenerator-runtime": "^0.14.0" @@ -1963,14 +1963,14 @@ } }, "node_modules/@babel/template": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.8.tgz", - "integrity": "sha512-iNKaX3ZebKIsCvJ+0jd6embf+Aulaa3vNBqZ41kM7iTWjx5qzWKXGHiJUW3+nTpQ18SG11hdF8OAzKrpXkb96Q==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", + "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.26.8", - "@babel/types": "^7.26.8" + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9" }, "engines": { "node": ">=6.9.0" @@ -1995,9 +1995,9 @@ } }, "node_modules/@babel/types": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.8.tgz", - "integrity": "sha512-eUuWapzEGWFEpHFxgEaBG8e3n6S8L3MSu0oda755rOfabWPnh0Our1AozNFVUxGFIhbKgd1ksprsoDGMinTOTA==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", + "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.25.9", diff --git a/src/components/FeaturedVideoCard/featuredVideoCard.tsx b/src/components/FeaturedVideoCard/featuredVideoCard.tsx index f4dfe00..5ad1105 100644 --- a/src/components/FeaturedVideoCard/featuredVideoCard.tsx +++ b/src/components/FeaturedVideoCard/featuredVideoCard.tsx @@ -6,6 +6,7 @@ import Typography from "@mui/material/Typography"; import Image from "next/image"; import useNavigation from "@/hooks/useNavigation"; +import { BASE_URL } from "@/utils/constants"; import { convertSecondsToFormattedTime, formatDateTime, trimTextLength } from "@/utils/utils"; import { FeaturedVideoCardContainer, ImageContainerBox } from "./styled"; @@ -23,7 +24,7 @@ const FeaturedVideoCard: FC = ({ video_duration, width = "100%", }) => { - const inAppThumbnail = `${process.env.NEXT_PUBLIC_BASE_URL ?? ""}/${thumbnail}`; + const inAppThumbnail = `${BASE_URL}/${thumbnail}`; const { navigateTo } = useNavigation(); return isVisible ? ( diff --git a/src/components/VideoCard/__snapshots__/videoCard.test.tsx.snap b/src/components/VideoCard/__snapshots__/videoCard.test.tsx.snap index 4fb6fc6..bb609d4 100644 --- a/src/components/VideoCard/__snapshots__/videoCard.test.tsx.snap +++ b/src/components/VideoCard/__snapshots__/videoCard.test.tsx.snap @@ -19,20 +19,22 @@ exports[`VideoCard should matches snapshot 1`] = ` style="width: 100%; height: 192px;" /> Sample Video Title
- 30:00 + 30:30
- Sample Video Title + Refresher & AMA Session on Competency Framework Changes - January 2025

- Ikram Ali + John Doe

- Oct 22, 2024 + Jan 09, 2025

diff --git a/src/components/VideoCard/types.ts b/src/components/VideoCard/types.ts index 27a4da9..94a2991 100644 --- a/src/components/VideoCard/types.ts +++ b/src/components/VideoCard/types.ts @@ -1,22 +1,12 @@ export type VideoCardProps = { className?: string; - id: number; - title: string; - description: string; - publisher: TPublisher; - event_time: string; - event_type: string; - status: string; - workstream_id: string; - is_featured: boolean; - video_duration: number; - tags: string[]; - thumbnail: string; + data: { + event_time: string; + organizer: string; + thumbnail: string; + title: string; + video_duration: string; + }; + onClick?: VoidFunction; width?: string; }; - -interface TPublisher { - id: number; - first_name: string; - last_name: string; -} diff --git a/src/components/VideoCard/videoCard.stories.tsx b/src/components/VideoCard/videoCard.stories.tsx index 60652a2..5bccb99 100644 --- a/src/components/VideoCard/videoCard.stories.tsx +++ b/src/components/VideoCard/videoCard.stories.tsx @@ -1,3 +1,4 @@ +import { faker } from "@faker-js/faker"; import type { Meta, StoryObj } from "@storybook/react"; import { VideoCardProps } from "./types"; @@ -8,29 +9,21 @@ const meta: Meta = { component: VideoCard, tags: ["autodocs"], argTypes: { - title: { + className: { control: "text", - description: "The title of the video", + description: "Additional CSS class for the video card", }, - workstream_id: { - control: "text", - description: "The organizer/creator of the video", - }, - event_time: { - control: "text", - description: "The upload date of the video", - }, - thumbnail: { - control: "text", - description: "The URL of the video thumbnail image", - }, - tags: { + data: { control: "object", - description: "The tags available in the video", + description: "The video data including title, organizer, event time, thumbnail, and duration", + }, + onClick: { + action: "clicked", + description: "Callback function triggered when the video card is clicked", }, width: { control: "text", - description: "The width of the card (e.g., '100%', '315px')", + description: "The width of the video card (e.g., '315px', '100%')", }, }, }; @@ -39,20 +32,51 @@ export default meta; type Story = StoryObj; +const mockVideoData = { + event_time: "2023-10-01 14:30", + organizer: "Sample Organizer", + thumbnail: faker.image.urlLoremFlickr(), + title: "Sample Video Title", + video_duration: "10:30", +}; + export const Default: Story = { args: { - title: "Sample Video Title", - workstream_id: "Arbisoft", - event_time: "2023-10-01", - thumbnail: "/assets/images/temp-youtube-logo.webp", - tags: ["Workshop", "Ollama", "AI"], + data: mockVideoData, + width: "315px", + }, +}; + +export const CustomWidth: Story = { + args: { + ...Default.args, + width: "400px", }, }; export const LongTitle: Story = { args: { ...Default.args, - title: "This is a very long video title that should be truncated", - width: "315px", + data: { + ...mockVideoData, + title: "This is a very long video title that should be truncated after two lines", + }, + }, +}; + +export const NoThumbnail: Story = { + args: { + ...Default.args, + data: { + ...mockVideoData, + thumbnail: "", + }, + }, +}; + +export const CustomClassName: Story = { + args: { + ...Default.args, + className: "custom-video-card", }, }; diff --git a/src/components/VideoCard/videoCard.test.tsx b/src/components/VideoCard/videoCard.test.tsx index 008cdb0..b3ec939 100644 --- a/src/components/VideoCard/videoCard.test.tsx +++ b/src/components/VideoCard/videoCard.test.tsx @@ -1,77 +1,61 @@ -import useNavigation from "@/hooks/useNavigation"; import { fireEvent, customRender as render, screen } from "@/jest/utils/testUtils"; +import { BASE_URL } from "@/utils/constants"; +import { convertSecondsToFormattedTime, formatDateTime } from "@/utils/utils"; import { VideoCardProps } from "./types"; import VideoCard from "./videoCard"; const mockProps: VideoCardProps = { - id: 1, className: "custom-class", - event_time: "2024-10-22T12:00:00Z", - event_type: "SESSION", - description: "Sample Video Description", - thumbnail: "assets/images/temp-youtube-logo.webp", - title: "Sample Video Title", - publisher: { id: 1, first_name: "John", last_name: "Doe" }, - tags: ["Workshop", "Ollama", "AI"], - is_featured: false, - status: "PUBLISHED", - workstream_id: "Ikram Ali", + data: { + title: "Refresher & AMA Session on Competency Framework Changes - January 2025", + event_time: formatDateTime("2025-01-09T06:00:00Z"), + thumbnail: `${BASE_URL}/media/thumbnails/Screenshot_2025-02-12_at_1.13.39PM.png`, + video_duration: convertSecondsToFormattedTime(1830), + organizer: "John Doe", + }, + onClick: jest.fn(), width: "300px", - video_duration: 1800, }; -jest.mock("@/hooks/useNavigation", () => ({ - __esModule: true, - default: jest.fn(() => ({ navigateTo: jest.fn() })), -})); - describe("VideoCard", () => { - const mockNavigateTo = jest.fn(); - - beforeEach(() => { - jest.clearAllMocks(); - (useNavigation as jest.Mock).mockReturnValue({ navigateTo: mockNavigateTo }); - }); - - test("should renders the component with provided props", () => { + it("should renders the component with provided props", () => { render(); - expect(screen.getByText(mockProps.title)).toBeInTheDocument(); - expect(screen.getByTestId("video-card-date-time")).toBeInTheDocument(); - const imgUrl = screen.getByRole("img", { name: mockProps.title }).getAttribute("src") ?? ""; - expect(decodeURIComponent(imgUrl)).toContain(mockProps.thumbnail); - expect(screen.getByText("30:00")).toBeInTheDocument(); + expect(screen.getByTestId("video-card-title")).toHaveTextContent(mockProps.data.title); + expect(screen.getByTestId("video-card-date-time")).toHaveTextContent(mockProps.data.event_time); + expect(screen.getByTestId("video-card-organizer")).toHaveTextContent(mockProps.data.organizer); + expect(screen.getByTestId("video-card-duration")).toHaveTextContent(mockProps.data.video_duration); + const imgUrl = screen.getByTestId("video-card-image").getAttribute("src") ?? ""; + expect(decodeURIComponent(imgUrl)).toContain(mockProps.data.thumbnail); }); - test("should displays default image when imgUrl is not provided", () => { - render(); - - const imgUrl = screen.getByRole("img", { name: mockProps.title }).getAttribute("src") ?? ""; + it("should displays default image when thumbnail is not provided", () => { + render(); + const imgUrl = screen.getByTestId("video-card-image").getAttribute("src") ?? ""; expect(decodeURIComponent(imgUrl)).toContain("/assets/images/temp-youtube-logo.webp"); }); - test("should renders the organizer name", () => { + it("should renders the organizer name", () => { render(); - expect(screen.getByText(mockProps.title)).toBeInTheDocument(); expect(screen.getByTestId("video-card-organizer")).toBeInTheDocument(); + expect(screen.getByTestId("video-card-organizer")).toHaveTextContent(mockProps.data.organizer); }); - test("should matches snapshot", () => { + it("should matches snapshot", () => { const { asFragment } = render(); expect(asFragment()).toMatchSnapshot(); }); - test("should calls navigateTo when clicked", () => { + it("should calls onClick when clicked", () => { render(); fireEvent.click(screen.getByTestId("video-card")); - expect(mockNavigateTo).toHaveBeenCalledWith("videoDetail", { id: 1 }); + expect(mockProps.onClick).toHaveBeenCalled(); }); - test("should displays fallback thumbnail when no thumbnail is provided", () => { - render(); - const imgUrl = screen.getByRole("img", { name: mockProps.title }).getAttribute("src") ?? ""; - expect(decodeURIComponent(imgUrl)).toContain(mockProps.thumbnail); + it("should renders skeleton loader correctly", () => { + render(); + expect(screen.getByTestId("video-card")).toBeInTheDocument(); }); }); diff --git a/src/components/VideoCard/videoCard.tsx b/src/components/VideoCard/videoCard.tsx index 11f73af..655a2af 100644 --- a/src/components/VideoCard/videoCard.tsx +++ b/src/components/VideoCard/videoCard.tsx @@ -1,5 +1,3 @@ -"use client"; - import React, { FC } from "react"; import Box from "@mui/material/Box"; @@ -8,54 +6,35 @@ import Skeleton from "@mui/material/Skeleton"; import Typography from "@mui/material/Typography"; import Image from "next/image"; -import useNavigation from "@/hooks/useNavigation"; -import { convertSecondsToFormattedTime, formatDateTime } from "@/utils/utils"; - import { ImageWrapper, VideoCardContainer } from "./styled"; import { VideoCardProps } from "./types"; -const VideoCard: FC = ({ - id, - className, - event_time, - thumbnail, - workstream_id, - title, - video_duration, - width = "315px", -}) => { - const { navigateTo } = useNavigation(); - const inAppThumbnail = `${process.env.NEXT_PUBLIC_BASE_URL ?? ""}/${thumbnail}`; - +const VideoCard: FC = ({ className, data, onClick, width = "315px" }) => { return ( - navigateTo("videoDetail", { id })} - > + {title} - - {convertSecondsToFormattedTime(video_duration)} + + {data.video_duration} - - {title} + + {data.title} - {workstream_id} + {data.organizer} - {formatDateTime(event_time)} + {data.event_time} diff --git a/src/features/VideosListingPage/videosListingPage.tsx b/src/features/VideosListingPage/videosListingPage.tsx index a5cb967..6ff5d33 100644 --- a/src/features/VideosListingPage/videosListingPage.tsx +++ b/src/features/VideosListingPage/videosListingPage.tsx @@ -18,7 +18,8 @@ import VideoCard from "@/components/VideoCard"; import useNavigation from "@/hooks/useNavigation"; import { Event, Tag, TAllEventsPyaload } from "@/models/Events"; import { useGetEventsQuery, useEventTagsQuery } from "@/redux/events/apiSlice"; -import { parseNonPassedParams } from "@/utils/utils"; +import { BASE_URL } from "@/utils/constants"; +import { convertSecondsToFormattedTime, formatDateTime, parseNonPassedParams } from "@/utils/utils"; import { FilterBox, TagsContainer, VideoListingContainer } from "./styled"; import { defaultParams, defaultTag } from "./types"; @@ -108,7 +109,22 @@ const VideosListingPage = () => { )) - : listedVideos?.map((videoCard) => )} + : listedVideos?.map((videoCard) => { + return ( + navigateTo("videoDetail", { id: videoCard.id })} + width="100%" + /> + ); + })} diff --git a/src/redux/customBaseQuery.ts b/src/redux/customBaseQuery.ts index 27b3cee..ba6ad45 100644 --- a/src/redux/customBaseQuery.ts +++ b/src/redux/customBaseQuery.ts @@ -2,12 +2,13 @@ import { fetchBaseQuery } from "@reduxjs/toolkit/query"; import type { BaseQueryFn, FetchArgs, FetchBaseQueryError } from "@reduxjs/toolkit/query"; import { notificationManager } from "@/components/Notification"; +import { BASE_URL } from "@/utils/constants"; import { selectAccessToken } from "./login/selectors"; import { parseError } from "./parseError"; import { ReducersState } from "./store/configureStore"; -const HOST_URL = (process.env.NEXT_PUBLIC_BASE_URL ?? "") + "/api/v1"; +const HOST_URL = BASE_URL + "/api/v1"; interface ExtraOptions { showErrorToast?: boolean; diff --git a/src/utils/constants.ts b/src/utils/constants.ts new file mode 100644 index 0000000..0aee866 --- /dev/null +++ b/src/utils/constants.ts @@ -0,0 +1 @@ +export const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL ?? ""; diff --git a/src/utils/utils.test.ts b/src/utils/utils.test.ts index 383d357..5020736 100644 --- a/src/utils/utils.test.ts +++ b/src/utils/utils.test.ts @@ -75,3 +75,25 @@ describe("parseNonPassedParams", () => { expect(parseNonPassedParams(input)).toEqual(expectedOutput); }); }); + +describe("BASE_URL constant", () => { + beforeEach(() => { + jest.resetModules(); // Reset module cache to re-import process.env values + delete process.env.NEXT_PUBLIC_BASE_URL; + }); + + test("should use NEXT_PUBLIC_BASE_URL when defined", () => { + process.env.NEXT_PUBLIC_BASE_URL = "https://example.com"; + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { BASE_URL } = require("./constants"); + + expect(BASE_URL).toBe("https://example.com"); + }); + + test("should default to an empty string when NEXT_PUBLIC_BASE_URL is undefined", () => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { BASE_URL } = require("./constants"); + + expect(BASE_URL).toBe(""); + }); +});