diff --git a/src/app/[language]/videos/results/page.tsx b/src/app/[language]/videos/results/page.tsx new file mode 100644 index 0000000..d04e8e3 --- /dev/null +++ b/src/app/[language]/videos/results/page.tsx @@ -0,0 +1,20 @@ +import type { Metadata } from "next"; + +import SearchResultsPage from "@/features/SearchResultsPage"; +import { getServerTranslation } from "@/services/i18n"; + +type Props = { + params: { language: string }; +}; + +export async function generateMetadata({ params }: Props): Promise { + const { t } = await getServerTranslation(params.language, "videos"); + + return { + title: t("search-title"), + }; +} + +export default function Results() { + return ; +} diff --git a/src/components/FeaturedVideoCard/__snapshots__/featuredVideoCard.test.tsx.snap b/src/components/FeaturedVideoCard/__snapshots__/featuredVideoCard.test.tsx.snap index c56b404..1a9f888 100644 --- a/src/components/FeaturedVideoCard/__snapshots__/featuredVideoCard.test.tsx.snap +++ b/src/components/FeaturedVideoCard/__snapshots__/featuredVideoCard.test.tsx.snap @@ -3,7 +3,7 @@ exports[`FeaturedVideoCard should match snapshot 1`] = `
diff --git a/src/components/FeaturedVideoCard/styled.tsx b/src/components/FeaturedVideoCard/styled.tsx index de60cf2..236123a 100644 --- a/src/components/FeaturedVideoCard/styled.tsx +++ b/src/components/FeaturedVideoCard/styled.tsx @@ -49,7 +49,7 @@ export const FeaturedVideoCardContainer = styled(Card, { -webkit-line-clamp: 2; color: ${theme.palette.colors.white}; display: -webkit-box; - font-size: 28px; + font-size: 26px; font-style: normal; font-weight: 500; letter-spacing: 0.4px; @@ -81,7 +81,7 @@ export const FeaturedVideoCardContainer = styled(Card, { } } - ${theme.breakpoints.down("xl")} { + ${theme.breakpoints.down("lg")} { .${cardContentClasses.root} { .video-detail { .${typographyClasses.h3} { diff --git a/src/components/Navbar/navbar.test.tsx b/src/components/Navbar/navbar.test.tsx index 7657b9f..887e998 100644 --- a/src/components/Navbar/navbar.test.tsx +++ b/src/components/Navbar/navbar.test.tsx @@ -119,8 +119,10 @@ describe("Navbar Component", () => { test("should navigate on search submit", () => { customRender(); - fireEvent.submit(screen.getByTestId("search-query")); - expect(mockNavigateTo).toHaveBeenCalledWith("videos", { search: "" }); + const searchInput = screen.getByPlaceholderText("Search..."); + fireEvent.change(searchInput, { target: { value: "some test search" } }); + fireEvent.submit(searchInput.closest("form")!); + expect(mockNavigateTo).toHaveBeenCalledWith("searchResult", { search: "some test search" }); }); test("should contain all user menu options", () => { @@ -147,14 +149,6 @@ describe("Navbar Component", () => { expect(searchInput).toHaveValue(""); }); - test("should navigate on search submit", () => { - customRender(); - const searchInput = screen.getByTestId("search-query"); - fireEvent.change(searchInput, { target: { value: "Test Query" } }); - fireEvent.submit(searchInput); - expect(mockNavigateTo).toHaveBeenCalledWith("videos", { search: "Test Query" }); - }); - test("should call logout and purge persistor when logout is clicked", async () => { customRender(); fireEvent.click(screen.getByTestId("avatar-btn")); diff --git a/src/components/Navbar/navbar.tsx b/src/components/Navbar/navbar.tsx index 4eb4008..792b412 100644 --- a/src/components/Navbar/navbar.tsx +++ b/src/components/Navbar/navbar.tsx @@ -51,7 +51,9 @@ function Navbar() { const handleSearch = (searchEvent: React.FormEvent) => { searchEvent.preventDefault(); - navigateTo("videos", { search: searchQuery }); + if (searchQuery.length > 0) { + navigateTo("searchResult", { search: searchQuery }); + } }; const handleClearSearch = () => { diff --git a/src/components/SearchVideoCard/searchVideoCard.tsx b/src/components/SearchVideoCard/searchVideoCard.tsx new file mode 100644 index 0000000..bbd99c2 --- /dev/null +++ b/src/components/SearchVideoCard/searchVideoCard.tsx @@ -0,0 +1,69 @@ +import React, { FC } from "react"; + +import Box from "@mui/material/Box"; +import CardContent from "@mui/material/CardContent"; +import Typography from "@mui/material/Typography"; +import Image from "next/image"; + +import useNavigation from "@/hooks/useNavigation"; +import { convertSecondsToFormattedTime, formatDateTime, trimTextLength } from "@/utils/utils"; + +import { SearchVideoCardContainer, ImageContainerBox } from "./styled"; +import { SearchVideoCardProps } from "./types"; + +const SearchVideoCard: FC = ({ + id, + className, + event_time, + thumbnail, + workstream_id, + title, + description, + video_duration, + width = "100%", +}) => { + const inAppThumbnail = `${process.env.NEXT_PUBLIC_BASE_URL ?? ""}/${thumbnail}`; + const { navigateTo } = useNavigation(); + + return ( + navigateTo("videoDetail", { id })} + > + + + {title} + + {convertSecondsToFormattedTime(video_duration)} + + + + + + {trimTextLength(title, 70)} + + + {workstream_id} + + + {event_time ? formatDateTime(event_time) : null} + + {description && ( + + {trimTextLength(description, 250)} + + )} + + + + ); +}; + +export default SearchVideoCard; diff --git a/src/components/SearchVideoCard/styled.tsx b/src/components/SearchVideoCard/styled.tsx new file mode 100644 index 0000000..a3a9997 --- /dev/null +++ b/src/components/SearchVideoCard/styled.tsx @@ -0,0 +1,133 @@ +import Box from "@mui/material/Box"; +import Card from "@mui/material/Card"; +import { cardContentClasses } from "@mui/material/CardContent"; +import { styled, css } from "@mui/material/styles"; +import { typographyClasses } from "@mui/material/Typography"; + +import { shouldForwardProp } from "@/utils/styleUtils"; + +export const SearchVideoCardContainer = styled(Card, { + name: "SearchVideoCardContainer", + shouldForwardProp, +})<{ $width: string }>(({ theme, $width }) => { + return css` + display: flex; + background-color: transparent; + background-image: unset; + border-radius: ${theme.shape.borderRadius + 8}px; + overflow: hidden; + width: ${$width}; + margin-top: 10px; + cursor: pointer; + + .${cardContentClasses.root} { + align-items: flex-start; + display: flex; + flex-direction: row; + gap: ${theme.spacing(2.5)}; + padding: 0; + :last-child { + padding: 0; + } + + img { + border-radius: ${theme.shape.borderRadius + 8}px; + height: auto; + object-fit: cover; + width: 400px; + } + + .video-detail { + width: calc(100% - 400px); + display: flex; + flex-direction: column; + gap: 7px; + + .${typographyClasses.h3} { + -webkit-line-clamp: 2; + color: ${theme.palette.colors.white}; + display: -webkit-box; + font-size: 28px; + font-style: normal; + font-weight: 500; + letter-spacing: 0.4px; + overflow: hidden; + text-overflow: ellipsis; + -webkit-box-orient: vertical; + } + + .date-time, + .organizer-name { + color: ${theme.palette.colors.gray}; + font-size: 18px; + font-style: normal; + font-weight: 500; + letter-spacing: 0.4px; + } + + .video-description { + -webkit-line-clamp: 3; + display: -webkit-box; + color: ${theme.palette.colors.white}; + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 29px; + letter-spacing: 0.4px; + padding-top: 10px; + overflow: hidden; + text-overflow: ellipsis; + -webkit-box-orient: vertical; + } + } + } + + ${theme.breakpoints.down("lg")} { + height: 100%; + + .${cardContentClasses.root} { + flex-direction: column; + + .video-detail { + width: 100%; + + .${typographyClasses.h3} { + font-size: 20px; + line-height: 24px; + } + .date-time, + .organizer-name { + font-size: 14px; + line-height: 14px; + } + .video-description { + font-size: 14px; + line-height: 14px; + } + } + } + } + `; +}); + +export const ImageContainerBox = styled(Box, { + name: "ImageContainerBox", + shouldForwardProp, +})(({ theme }) => { + return css` + position: relative; + display: inline-flex; + + .video-duration { + background-color: rgba(0, 0, 0, 0.7); + border-radius: 2px; + bottom: 10px; + color: ${theme.palette.colors.white}; + font-size: 12px; + padding: 2px 4px; + position: absolute; + right: 10px; + z-index: 9999; + } + `; +}); diff --git a/src/components/SearchVideoCard/types.ts b/src/components/SearchVideoCard/types.ts new file mode 100644 index 0000000..93d32a2 --- /dev/null +++ b/src/components/SearchVideoCard/types.ts @@ -0,0 +1,11 @@ +export type SearchVideoCardProps = { + id: number; + className?: string; + event_time: string; + thumbnail?: string; + title: string; + description: string; + video_duration: number; + workstream_id: string; + width?: string; +}; diff --git a/src/features/SearchResultsPage/index.tsx b/src/features/SearchResultsPage/index.tsx new file mode 100644 index 0000000..d773de2 --- /dev/null +++ b/src/features/SearchResultsPage/index.tsx @@ -0,0 +1,3 @@ +import SearchResultsPage from "./searchResultsPage"; + +export default SearchResultsPage; diff --git a/src/features/SearchResultsPage/searchResultsPage.tsx b/src/features/SearchResultsPage/searchResultsPage.tsx new file mode 100644 index 0000000..d8a40a8 --- /dev/null +++ b/src/features/SearchResultsPage/searchResultsPage.tsx @@ -0,0 +1,104 @@ +"use client"; + +import React, { useEffect, useState } from "react"; + +import { faker } from "@faker-js/faker"; +import Box from "@mui/material/Box"; +import Skeleton from "@mui/material/Skeleton"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { useSearchParams } from "next/navigation"; + +import MainLayoutContainer from "@/components/containers/MainLayoutContainer"; +import SearchVideoCard from "@/components/SearchVideoCard/searchVideoCard"; +import Select from "@/components/Select"; +import { TAllEventsPyaload } from "@/models/Events"; +import { useGetEventsQuery } from "@/redux/events/apiSlice"; +import { parseNonPassedParams } from "@/utils/utils"; + +import { FilterBox, NoSearchResultsWrapper, SearchCardLoadingState, SearchResultsContainer } from "./styled"; +import { defaultParams } from "./types"; + +const selectMenuItems: string[] = Array(3) + .fill("") + .map(() => faker.lorem.words(1)); + +const loaderCards: string[] = Array(5) + .fill("") + .map(() => faker.lorem.words(1)); + +const SearchResultsPage = () => { + const searchParams = useSearchParams(); + + const [searchedQuery, setSearchedQuery] = useState(""); + const [requestParams, setRequestParams] = useState(defaultParams); + const { data: videoListings, isFetching, isLoading, isUninitialized, error } = useGetEventsQuery(requestParams); + + const isDataLoading = isFetching || isLoading || isUninitialized; + + useEffect(() => { + const search = searchParams?.get("search") as string; + setSearchedQuery(search); + + setRequestParams((prev) => { + const apiParams = { ...prev }; + if (search) { + apiParams.tag = ""; + apiParams.search = search; + } + const updatedParams = parseNonPassedParams(apiParams) as TAllEventsPyaload; + return updatedParams; + }); + }, [searchParams]); + + const renderResults = () => { + if (error || isDataLoading) { + return loaderCards.map((_) => ( + + + + +
+ + +
+ + + +
+
+ )); + } + + if (videoListings?.results?.length) { + return videoListings?.results?.map((videoCard) => ); + } + + return ( + + + No videos found for '{searchedQuery}' + + + ); + }; + + return ( + + + + + Showing search results for '{searchedQuery}' + +