Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DR-3363: Search result card #284

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
69 changes: 69 additions & 0 deletions __tests__/__mocks__/data/mockSearchCards.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import SearchCardType from "@/src/types/SearchCardType";

export const mockSearchCards: SearchCardType[] = [
{
uuid: "60932400-20f2-0138-8583-05c43d448773",
title: "Posada Collection",
url: "https://digitalcollections.nypl.org/collections/posada-collection#/?tab=navigation",
imageID: "58270299",
imageURL:
"https://iiif.nypl.org/iiif/2/58270299/square/!288,288/0/default.jpg",
numberOfDigitizedItems: 34,
containsOnSiteMaterial: true,
recordType: "Collection",
containsAVMaterial: false,
contentType: null,
highlights: [
"Christopher Walken examples in the stage production example Kid Champion",
],
firstIndexed: "1907-01-01T00:00:00Z",
},
{
uuid: "50370c90-cabb-013c-da64-0242ac110002",
title: "Sarah Myers Blackwell sitting in a tree",
url: "https://digitalcollections.nypl.org/items/50370c90-cabb-013c-da64-0242ac110002",
imageID: "58886955",
imageURL:
"https://iiif.nypl.org/iiif/2/58886955/square/!288,288/0/default.jpg",
numberOfDigitizedItems: 1,
containsOnSiteMaterial: false,
recordType: "Item",
containsAVMaterial: false,
containsMultipleCaptures: false,
contentType: "Image",
highlights: ["Sarah example sitting in tree"],
firstIndexed: "1907-02-01T00:00:00Z",
},
{
uuid: "e5462600-c5d9-012f-a6a3-58d385a7bc34",
title: "Farm Security Administration Photographs",
url: "https://digitalcollections.nypl.org/collections/farm-security-administration-photographs#/?tab=navigation",
imageID: "1952272",
imageURL:
"https://iiif.nypl.org/iiif/2/1952272/square/!288,288/0/default.jpg",
numberOfDigitizedItems: 36,
containsOnSiteMaterial: false,
recordType: "Sub-collection",
containsAVMaterial: true,
contentType: null,
highlights: ["Farm in example photographs"],
firstIndexed: "1907-02-01T00:00:00Z",
},
{
uuid: "12563fb0-63a2-013b-bd44-0242ac110003",
title:
"Reading room of the Schomburg Collection at the 135th Street Branch Library. Lawrence Reddick, curator, seated at right",
imageID: "58613608",
url: "https://digitalcollections.nypl.org/items/12563fb0-63a2-013b-bd44-0242ac110003",
imageURL:
"https://iiif.nypl.org/iiif/2/58613608/square/!288,288/0/default.jpg",
recordType: "Item",
numberOfDigitizedItems: 1,
containsOnSiteMaterial: true,
containsAVMaterial: false,
containsMultipleCaptures: true,
contentType: "Image",
highlights: ["Reading in example room"],
firstIndexed: "1908-02-01T00:00:00Z",
},
];
69 changes: 69 additions & 0 deletions __tests__/__mocks__/data/mockSearchResponse.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
export const mockSearchResponse = {
keyword: "example",
numResults: 4,
page: 1,
perPage: 48,
results: [
{
uuid: "60932400-20f2-0138-8583-05c43d448773",
recordType: "collection",
title: "Posada Collection",
imageID: "58270299",
numberOfDigitizedItems: 34,
containsOnSiteMaterial: true,
containsAVMaterial: false,
contentType: "image",
highlights: {
mainTitle_st: [
"Christopher Walken examples in the stage production example Kid Champion",
],
},
firstIndexed_dt: "1907-01-01T00:00:00Z",
},
{
uuid: "50370c90-cabb-013c-da64-0242ac110002",
recordType: "item",
title: "Sarah Myers Blackwell sitting in a tree",
imageID: "58886955",
numberOfDigitizedItems: 1,
containsOnSiteMaterial: false,
containsAVMaterial: false,
containsMultipleCaptures: false,
contentType: "image",
highlights: {
mainTitle_st: ["Sarah example sitting in tree"],
},
firstIndexed_dt: "1907-02-01T00:00:00Z",
},
{
uuid: "e5462600-c5d9-012f-a6a3-58d385a7bc34",
recordType: "sub-collection",
title: "Farm Security Administration Photographs",
imageID: "1952272",
numberOfDigitizedItems: 36,
containsOnSiteMaterial: false,
containsAVMaterial: false,
contentType: null,
highlights: {
mainTitle_st: ["Farm in example photographs"],
},
firstIndexed_dt: "1907-02-01T00:00:00Z",
},
{
uuid: "12563fb0-63a2-013b-bd44-0242ac110003",
recordType: "item",
title:
"Reading room of the Schomburg Collection at the 135th Street Branch Library. Lawrence Reddick, curator, seated at right",
imageID: "58613608",
numberOfDigitizedItems: 1,
containsOnSiteMaterial: false,
containsAVMaterial: false,
containsMultipleCaptures: true,
contentType: "image",
highlights: {
mainTitle_st: ["Reading in example room"],
},
firstIndexed_dt: "1908-02-01T00:00:00Z",
},
],
};
7 changes: 2 additions & 5 deletions app/collections/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from "react";
import { Metadata } from "next";
import PageLayout from "../../src/components/pageLayout/pageLayout";
import { mockItems } from "__tests__/__mocks__/data/mockItems";
import { createAdobeAnalyticsPageName } from "@/src/utils/utils";
import CollectionPage from "@/src/components/pages/collectionPage/collectionPage";
import { mockSearchResponse } from "__tests__/__mocks__/data/mockSearchResponse";

type CollectionProps = {
params: { slug: string };
Expand Down Expand Up @@ -38,10 +38,7 @@ export default function Collection({ params }: CollectionProps) {
params.slug
)}
>
<CollectionPage
slug={"Example collection"}
data={[...mockItems, ...mockItems]}
/>
<CollectionPage slug={"Example collection"} data={mockSearchResponse} />
</PageLayout>
);
}
3 changes: 2 additions & 1 deletion app/search/index/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import PageLayout from "../../src/components/pageLayout/pageLayout";
import { mockItems } from "../../../__tests__/__mocks__/data/mockItems"; // TODO: render mockItems
import { createAdobeAnalyticsPageName } from "@/src/utils/utils";
import SearchPage from "@/src/components/pages/searchPage/searchPage";
import { mockSearchResponse } from "__tests__/__mocks__/data/mockSearchResponse";

export type SearchProps = {
params: { slug: string };
Expand All @@ -22,7 +23,7 @@ export default async function Search() {
""
)} //TODO: if there are no query params, page name should be createAdobeAnalyticsPageName("all-items", "")
>
<SearchPage data={[...mockItems, ...mockItems]} />
<SearchPage data={mockSearchResponse} />
</PageLayout>
);
}
68 changes: 68 additions & 0 deletions app/src/components/card/searchCard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { render, screen } from "@testing-library/react";
import { SearchCard } from "./searchCard";
import { mockSearchCards } from "__tests__/__mocks__/data/mockSearchCards";

const collectionResultData = mockSearchCards[0];
const itemResultData = mockSearchCards[3];

describe("Search card displaying collection result", () => {
it("renders the correct heading with the provided title", () => {
render(<SearchCard result={collectionResultData} keywords={[]} />);
const headingElement = screen.getByRole("heading", {
name: collectionResultData.title,
});
expect(headingElement).toBeInTheDocument();
});

it("renders the badge when containsOnSiteMaterial is true", () => {
render(<SearchCard result={collectionResultData} keywords={[]} />);
const badgeElement = screen.getByText(/Contains on-site materials/i);
expect(badgeElement).toBeInTheDocument();
});

it("wraps card in the correct link", () => {
render(<SearchCard result={collectionResultData} keywords={[]} />);
const link = screen.getByRole("link");
expect(link).toHaveAttribute(
"href",
expect.stringContaining("/collections/posada-collection#/?tab=navigation")
);
});

it("renders the correct content type tag", () => {
render(<SearchCard result={collectionResultData} keywords={[]} />);
const tagElement = screen.getByText("Collection");
expect(tagElement).toBeInTheDocument();
});
});

describe("Search card displaying item result", () => {
it("renders the correct heading with the provided title", () => {
render(<SearchCard result={itemResultData} keywords={[]} />);
const headingElement = screen.getByRole("heading", {
name: itemResultData.title,
});
expect(headingElement).toBeInTheDocument();
});

it("wraps card in the correct link", () => {
render(<SearchCard result={itemResultData} keywords={[]} />);
const link = screen.getByRole("link");
expect(link).toHaveAttribute(
"href",
expect.stringContaining("items/12563fb0-63a2-013b-bd44-0242ac110003")
);
});

it("renders the badge when containsOnSiteMaterial is true", () => {
render(<SearchCard result={itemResultData} keywords={[]} />);
const badgeElement = screen.getByText(/Available onsite only/i);
expect(badgeElement).toBeInTheDocument();
});

it("renders the correct content type tag", () => {
render(<SearchCard result={itemResultData} keywords={[]} />);
const tagElement = screen.getByText("Multiple images");
expect(tagElement).toBeInTheDocument();
});
});
113 changes: 113 additions & 0 deletions app/src/components/card/searchCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"use client";
import React from "react";
import {
Card,
CardHeading,
CardContent,
StatusBadge,
TagSet,
Flex,
} from "@nypl/design-system-react-components";
import SearchCardType, {
SearchResultRecordType,
} from "@/src/types/SearchCardType";

export interface SearchCardProps {
result: SearchCardType;
keywords: string;
}

const onSiteMaterialBadge = (recordType: SearchResultRecordType) => {
return (
<StatusBadge sx={{ margin: "0p" }} type="informative">
{recordType === "Item"
? "Available onsite only"
: "Contains on-site materials"}
</StatusBadge>
);
};

const contentTypeTag = (result: SearchCardType) => {
const displayLabel =
result.recordType === "Item"
? result.contentType === "Image" && result.containsMultipleCaptures
? "Multiple images"
: result.contentType
: result.recordType;

return (
<TagSet
onClick={() => {}}
tagSetData={[
{ id: `type-${result.uuid}`, label: displayLabel ? displayLabel : "" },
]}
type="filter"
sx={{ margin: 0 }}
/>
);
};

const highlightedText = ({ text, keyword }) => {
if (!text || !keyword) return text;
const words = text.split(" ");
const keywords = keyword.split(" ");
return (
<span>
{words.map((word, index) => {
const isKeyword = keywords.some(
(keyword) => keyword.toLowerCase() === word.toLowerCase()
);

return (
<span key={index}>
{isKeyword ? (
<span style={{ backgroundColor: "yellow" }}>{word}</span>
) : (
word
)}
{index < words.length - 1 ? " " : ""}
</span>
);
})}
</span>
);
};

export const SearchCard = ({ result, keywords }: SearchCardProps) => {
return (
<Card
id={result.uuid}
imageProps={{
alt: "",
aspectRatio: "sixteenByNine",
id: result.imageID
? `image-${result.imageID}`
: `no-image-${result.imageID}`,
isAtEnd: false,
isLazy: true,
size: "default",
src: result.imageURL,
}}
layout="row"
mainActionLink={result.url}
>
<CardHeading level="h3" size="heading5" marginBottom="xs">
{result.title}
</CardHeading>
<CardContent>
<Flex flexDir="column" gap="xs">
{result.containsOnSiteMaterial &&
onSiteMaterialBadge(result.recordType)}
{keywords?.length > 0 &&
highlightedText({
text: result.highlights[0],
keyword: keywords,
})}
{contentTypeTag(result)}
</Flex>
</CardContent>
</Card>
);
};

export default SearchCard;
18 changes: 18 additions & 0 deletions app/src/components/grids/searchCardsGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { SearchCardModel } from "@/src/models/searchCard";
import SearchCard from "../card/searchCard";
import { SimpleGrid } from "@nypl/design-system-react-components";

const SearchCardsGrid = ({ results, keywords }) => {
return (
<SimpleGrid columns={1} gap="grid.m">
{results?.map((result, index) => {
const searchResult = new SearchCardModel(result);
return (
<SearchCard key={index} keywords={keywords} result={searchResult} />
);
})}
</SimpleGrid>
);
};

export default SearchCardsGrid;
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { CardsGrid } from "../../grids/cardsGrid";
import React, { useEffect, useRef, useState } from "react";
import PageLayout from "../../pageLayout/pageLayout";
import LaneLoading from "../../lane/laneLoading";
import { CARDS_PER_PAGE } from "@/src/config/constants";

export default function CollectionLanePage({ data }: any) {
const params = useParams();
Expand All @@ -38,7 +39,7 @@ export default function CollectionLanePage({ data }: any) {

const { push } = useRouter();

const totalPages = totalNumPages(data.numResults, data.perPage);
const totalPages = totalNumPages(data.numResults, CARDS_PER_PAGE);

const headingRef = useRef<HTMLHeadingElement>(null);

Expand Down
Loading