From 924ffae07e93fc41cc72452a34bcb5ed7991cd11 Mon Sep 17 00:00:00 2001 From: Rishi Ghan Date: Tue, 22 Jul 2025 18:46:25 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=85=F0=9F=8F=BC=20Prettifying=20search?= =?UTF-8?q?=20results?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 + src/client/components/Search/Search.tsx | 259 ++++++++------- .../components/WantedComics/WantedComics.tsx | 295 ++++++++---------- src/client/components/shared/Carda.tsx | 29 +- .../components/shared/MetadataPanel.tsx | 176 +++++------ .../components/shared/PopoverButton.tsx | 4 +- src/client/components/shared/T2Table.tsx | 2 +- yarn.lock | 17 + 8 files changed, 407 insertions(+), 377 deletions(-) diff --git a/package.json b/package.json index 796ba86..b0eb804 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,8 @@ "final-form": "^4.20.2", "final-form-arrays": "^3.0.2", "focus-trap-react": "^10.2.3", + "graphql": "^16.0.0", + "graphql-request": "^7.2.0", "history": "^5.3.0", "html-to-text": "^8.1.0", "i18next": "^23.11.1", diff --git a/src/client/components/Search/Search.tsx b/src/client/components/Search/Search.tsx index d755bcb..a41279e 100644 --- a/src/client/components/Search/Search.tsx +++ b/src/client/components/Search/Search.tsx @@ -266,55 +266,80 @@ export const Search = ({}: ISearchProps): ReactElement => { )} {!isEmpty(comicVineSearchResults?.data?.results) ? ( -
+
{comicVineSearchResults.data.results.map((result) => { return result.resource_type === "issue" ? (
-
-
- + {/* IMAGE */} + + + {/* RIGHT-SIDE CONTENT */} +
+ {/* TITLE */} +
+ {result.volume?.name || No Name}
-
-
- {!isEmpty(result.volume.name) ? ( - result.volume.name - ) : ( - No Name - )} -
+ + {/* SUBMETA */} +
+ {/* Cover Date Token */} {result.cover_date && ( -

- Cover date - {dayjs(result.cover_date).format("MMM D, YYYY")} -

+ + + + + + {dayjs(result.cover_date).format("MMM YYYY")} + + )} -

{result.id}

+ {/* ID Token */} + + + + + + {result.id} + + +
- - {result.api_detail_url} - -

+ {/* LINK */} + + {result.api_detail_url} + + + {/* DESCRIPTION */} + {result.description && ( +

{ellipsize( - convert(result.description, { - baseElements: { - selectors: ["p", "div"], - }, + convert(result.description ?? "", { + baseElements: { selectors: ["p", "div"] }, }), - 320, + 300, )}

-
+ )} + {/* CTA BUTTON */} + {result.volume.name ? ( +
addToWantedList({ source: "comicvine", @@ -325,114 +350,106 @@ export const Search = ({}: ISearchProps): ReactElement => { } />
-
+ ) : null}
) : ( result.resource_type === "volume" && (
-
-
- + {/* LEFT COLUMN: COVER */} + + + {/* RIGHT COLUMN */} +
+ {/* TITLE */} +
+ {result.name || No Name} + {result.start_year && <> ({result.start_year})}
-
-
- {!isEmpty(result.name) ? ( - result.name - ) : ( - No Name - )} - {result.start_year && <> ({result.start_year})} -
-
- {/* issue count */} - {result.count_of_issues && ( -
- - - - + {/* TOKENS */} +
+ {/* ISSUE COUNT */} + {result.count_of_issues && ( + + + + + + {t("issueWithCount", { + count: result.count_of_issues, + })} + + + )} - - {t("issueWithCount", { - count: result.count_of_issues, - })} - + {/* FORMAT DETECTED */} + {result.description && + !isEmpty(detectIssueTypes(result.description)) && ( + + + -
+ + { + detectIssueTypes(result.description) + .displayName + } + +
)} - {/* type: TPB, one-shot, graphic novel etc. */} - {!isNil(result.description) && - !isUndefined(result.description) && ( - <> - {!isEmpty( - detectIssueTypes(result.description), - ) && ( -
- - - - - - { - detectIssueTypes(result.description) - .displayName - } - - -
- )} - - )} -
+ {/* ID */} + + + + + {result.id} + +
- {result.id} -

- - {result.api_detail_url} - -

+ {/* LINK */} + + {result.api_detail_url} + - {/* description */} -

+ {/* DESCRIPTION */} + {result.description && ( +

{ellipsize( convert(result.description, { - baseElements: { - selectors: ["p", "div"], - }, + baseElements: { selectors: ["p", "div"] }, }), 320, )}

-
- - addToWantedList({ - source: "comicvine", - comicObject: result, - markEntireVolumeWanted: true, - resourceType: "volume", - }) - } - /> -
-
+ )} + + + addToWantedList({ + source: "comicvine", + comicObject: result, + markEntireVolumeWanted: false, + resourceType: "issue", + }) + } + />
) diff --git a/src/client/components/WantedComics/WantedComics.tsx b/src/client/components/WantedComics/WantedComics.tsx index 4f06343..4c1277e 100644 --- a/src/client/components/WantedComics/WantedComics.tsx +++ b/src/client/components/WantedComics/WantedComics.tsx @@ -1,37 +1,87 @@ -import React, { ReactElement, useCallback, useEffect, useMemo } from "react"; -import SearchBar from "../Library/SearchBar"; +import React from "react"; +import { useQuery } from "@tanstack/react-query"; +import { gql, GraphQLClient } from "graphql-request"; import T2Table from "../shared/T2Table"; import MetadataPanel from "../shared/MetadataPanel"; -import { useQuery } from "@tanstack/react-query"; -import axios from "axios"; -import { SEARCH_SERVICE_BASE_URI } from "../../constants/endpoints"; -export const WantedComics = (props): ReactElement => { - const { - data: wantedComics, - isSuccess, - isFetched, - isError, - isLoading, - } = useQuery({ - queryFn: async () => - await axios({ - url: `${SEARCH_SERVICE_BASE_URI}/searchIssue`, - method: "POST", - data: { - query: {}, +/** + * GraphQL client for interfacing with Moleculer Apollo server. + */ +const client = new GraphQLClient("http://localhost:3000/graphql"); - pagination: { - size: 25, - from: 0, - }, - type: "wanted", - trigger: "wantedComicsPage", - }, - }), +/** + * GraphQL query to fetch wanted comics. + */ +const WANTED_COMICS_QUERY = gql` + query { + wantedComics(limit: 25, offset: 0) { + total + comics + } + } +`; + +/** + * Shape of an individual comic returned by the backend. + */ +type Comic = { + _id: string; + sourcedMetadata?: { + comicvine?: { + name?: string; + start_year?: string; + publisher?: { + name?: string; + }; + }; + }; + acquisition?: { + directconnect?: { + downloads?: Array<{ + name: string; + }>; + }; + }; +}; + +/** + * Shape of the GraphQL response returned for wanted comics. + */ +type WantedComicsResponse = { + wantedComics: { + total: number; + comics: Comic[]; + }; +}; + +/** + * React component rendering the "Wanted Comics" table using T2Table. + * Fetches data from GraphQL backend via graphql-request + TanStack Query. + * + * @component + * @returns {JSX.Element} React component + */ +const WantedComics = (): JSX.Element => { + const { data, isLoading, isError, isSuccess, error } = useQuery< + WantedComicsResponse["wantedComics"] + >({ queryKey: ["wantedComics"], - enabled: true, + queryFn: async () => { + const res = await client.request( + WANTED_COMICS_QUERY, + ); + + if (!res?.wantedComics?.comics) { + throw new Error("No comics returned"); + } + + return res.wantedComics; + }, + retry: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, }); + const columnData = [ { header: "Comic Information", @@ -40,11 +90,11 @@ export const WantedComics = (props): ReactElement => { header: "Details", id: "comicDetails", minWidth: 350, - accessorFn: (data) => data, - cell: (value) => { - console.log("ASDASd", value); - const row = value.getValue()._source; - return row && ; + accessorFn: (data: Comic) => data, + cell: (value: any) => { + const row = value.getValue(); + console.log("Comic row data:", row); + return row ? : null; }, }, ], @@ -55,148 +105,73 @@ export const WantedComics = (props): ReactElement => { { header: "Files", align: "right", - accessorKey: "_source.acquisition", - cell: (props) => { - const { - directconnect: { downloads }, - } = props.getValue(); - return ( -
- {downloads.length > 0 ? ( - {downloads.length} - ) : null} -
- ); + accessorFn: (row: Comic) => + row?.acquisition?.directconnect?.downloads || [], + cell: (props: any) => { + const downloads = props.getValue(); + return downloads?.length > 0 ? ( + {downloads.length} + ) : null; }, }, { header: "Download Details", id: "downloadDetails", - accessorKey: "_source.acquisition", - cell: (data) => ( + accessorFn: (row: Comic) => + row?.acquisition?.directconnect?.downloads || [], + cell: (data: any) => (
    - {data.getValue().directconnect.downloads.map((download, idx) => { - return ( -
  1. - {download.name} -
  2. - ); - })} + {data.getValue()?.map((download: any, idx: number) => ( +
  3. + {download.name} +
  4. + ))}
), }, - { - header: "Type", - id: "dcc", - }, ], }, ]; - /** - * Pagination control that fetches the next x (pageSize) items - * based on the y (pageIndex) offset from the Elasticsearch index - * @param {number} pageIndex - * @param {number} pageSize - * @returns void - * - **/ - // const nextPage = useCallback((pageIndex: number, pageSize: number) => { - // dispatch( - // searchIssue( - // { - // query: {}, - // }, - // { - // pagination: { - // size: pageSize, - // from: pageSize * pageIndex + 1, - // }, - // type: "wanted", - // trigger: "wantedComicsPage", - // }, - // ), - // ); - // }, []); - - /** - * Pagination control that fetches the previous x (pageSize) items - * based on the y (pageIndex) offset from the Elasticsearch index - * @param {number} pageIndex - * @param {number} pageSize - * @returns void - **/ - // const previousPage = useCallback((pageIndex: number, pageSize: number) => { - // let from = 0; - // if (pageIndex === 2) { - // from = (pageIndex - 1) * pageSize + 2 - 17; - // } else { - // from = (pageIndex - 1) * pageSize + 2 - 16; - // } - // dispatch( - // searchIssue( - // { - // query: {}, - // }, - // { - // pagination: { - // size: pageSize, - // from, - // }, - // type: "wanted", - // trigger: "wantedComicsPage", - // }, - // ), - // ); - // }, []); - return ( -
-
-
-
-
-
-

- Wanted Comics -

+
+
+
+
+
+

+ Wanted Comics +

+

+ Browse through comics you marked as "wanted." +

+
+
+
+
-

- Browse through comics you marked as "wanted." -

-
-
-
-
- {isSuccess && wantedComics?.data.hits?.hits ? ( -
-
- {}, - previousPage: () => {}, - }} - // rowClickHandler={navigateToComicDetail} - /> - {/* pagination controls */} -
-
- ) : null} - {isLoading ?
Loading...
: null} - {isError ? ( -
An error occurred while retrieving the pull list.
- ) : null} -
-
+ {isLoading && ( +
+ {Array.from({ length: 5 }).map((_, idx) => ( +
+ ))} +
+ )} + {isError &&
Error fetching wanted comics. {error?.message}
} + {isSuccess && data?.comics?.length > 0 ? ( + + ) : isSuccess ? ( +
No comics found.
+ ) : null} + ); }; diff --git a/src/client/components/shared/Carda.tsx b/src/client/components/shared/Carda.tsx index f6a1390..79e65a0 100644 --- a/src/client/components/shared/Carda.tsx +++ b/src/client/components/shared/Carda.tsx @@ -140,14 +140,31 @@ const renderCard = (props: ICardProps): ReactElement => { ); case "cover-only": + const containerStyle = { + width: props.cardContainerStyle?.width || "100%", + height: props.cardContainerStyle?.height || "auto", + maxWidth: props.cardContainerStyle?.maxWidth || "none", + ...props.cardContainerStyle, + }; + + const imageStyle = { + width: "100%", + height: "100%", + objectFit: "cover", + ...props.imageStyle, + }; + return ( - <> - {/* thumbnail */} -
- -
- +
+ Comic cover +
); + case "card-with-info-panel": return ( <> diff --git a/src/client/components/shared/MetadataPanel.tsx b/src/client/components/shared/MetadataPanel.tsx index 781658d..9426f91 100644 --- a/src/client/components/shared/MetadataPanel.tsx +++ b/src/client/components/shared/MetadataPanel.tsx @@ -1,32 +1,54 @@ import React, { ReactElement } from "react"; -import PropTypes from "prop-types"; import ellipsize from "ellipsize"; import prettyBytes from "pretty-bytes"; import { Card } from "../shared/Carda"; import { convert } from "html-to-text"; import { determineCoverFile } from "../../shared/utils/metadata.utils"; import { find, isUndefined } from "lodash"; +import { o } from "react-router/dist/development/fog-of-war-BLArG-qZ"; -interface IMetadatPanelProps { - value: any; - children: any; - imageStyle: any; - titleStyle: any; - tagsStyle: any; - containerStyle: any; +/** + * Props for the MetadataPanel component. + */ +interface MetadataPanelProps { + /** + * Comic metadata object passed into the panel. + */ + data: any; + + /** + * Optional custom styling for the cover image. + */ + imageStyle?: React.CSSProperties; + + /** + * Optional custom styling for the title section. + */ + titleStyle?: React.CSSProperties; } -export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => { + +/** + * MetadataPanel component + * + * Displays structured comic metadata based on the best available source + * (raw file data, ComicVine, or League of Comic Geeks). + * + * @component + * @param {MetadataPanelProps} props + * @returns {ReactElement} + */ +export const MetadataPanel = (props: MetadataPanelProps): ReactElement => { const { rawFileDetails, inferredMetadata, sourcedMetadata: { comicvine, locg }, } = props.data; + const { issueName, url, objectReference } = determineCoverFile({ comicvine, locg, rawFileDetails, }); - const metadataContentPanel = [ { name: "rawFileDetails", @@ -43,48 +65,29 @@ export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => { - {/* Issue number */} {inferredMetadata.issue.number && (
- - - - - {inferredMetadata.issue.number} - + + {inferredMetadata.issue.number}
)} +
- {/* File extension */} - - - - - - {rawFileDetails.mimeType} - + + {rawFileDetails.mimeType} - {/* size */} - - - - - - {prettyBytes(rawFileDetails.fileSize)} - + + {prettyBytes(rawFileDetails.fileSize)} - {/* Uncompressed version available? */} {rawFileDetails.archive?.uncompressed && ( - - - + )}
@@ -94,49 +97,51 @@ export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => { { name: "comicvine", - content: () => - !isUndefined(comicvine) && - !isUndefined(comicvine.volumeInformation) && ( -
-
-
- {ellipsize(issueName, 18)} -
-
-
- - Is a part of{" "} - - {comicvine.volumeInformation.name} + content: () => { + console.log("comicvine:", comicvine); + console.log("volumeInformation:", comicvine?.volumeInformation); + return ( + !isUndefined(comicvine?.volumeInformation) && ( +
+
+
+ {ellipsize(issueName, 18)} +
+
+
+ + Is a part of{" "} + + {comicvine.volumeInformation.name} + - -
-
- - {ellipsize( - convert(comicvine.description, { - baseElements: { - selectors: ["p"], - }, - }), - 120, - )} - -
-
- - {comicvine.volumeInformation.start_year} - - {comicvine.volumeInformation.count_of_issues} - ComicVine ID - {comicvine.id} -
-
- ), +
+
+ + {ellipsize( + convert(comicvine.description || "", { + baseElements: { selectors: ["p"] }, + }), + 120, + )} + +
+
+ + {comicvine.volumeInformation.start_year} + + {comicvine.volumeInformation.count_of_issues} + ComicVine ID: {comicvine.id} +
+
+ ) + ); + }, }, + { name: "locg", content: () => ( @@ -147,23 +152,22 @@ export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => {
- {ellipsize(locg.description, 120)} + {ellipsize(locg?.description || "", 120)}
-
- {locg.price} + {locg?.price} - {locg.pulls} + {locg?.pulls}
rating - {locg.rating} + {locg?.rating}
@@ -173,20 +177,18 @@ export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => { }, ]; - // Find the panel to display const metadataPanel = find(metadataContentPanel, { name: objectReference, }); - return (
-
{metadataPanel.content()}
+
{metadataPanel?.content()}
); }; diff --git a/src/client/components/shared/PopoverButton.tsx b/src/client/components/shared/PopoverButton.tsx index 1649ad2..2e17731 100644 --- a/src/client/components/shared/PopoverButton.tsx +++ b/src/client/components/shared/PopoverButton.tsx @@ -22,7 +22,7 @@ const PopoverButton = ({ content, clickHandler }) => { onFocus={() => setIsVisible(true)} onBlur={() => setIsVisible(false)} aria-describedby="popover" - className="flex space-x-1 sm:mt-0 sm:flex-row sm:items-center rounded-lg border border-green-400 dark:border-green-200 bg-green-200 px-2 py-2 text-gray-500 hover:bg-transparent hover:text-green-600 focus:outline-none focus:ring active:text-indigo-500" + className="flex text-sm space-x-1 sm:mt-0 sm:flex-row sm:items-center rounded-lg border border-green-400 dark:border-green-200 bg-green-200 px-1.5 py-1 text-gray-500 hover:bg-transparent hover:text-green-600 focus:outline-none focus:ring active:text-indigo-500" onClick={clickHandler} > {" "} @@ -32,7 +32,7 @@ const PopoverButton = ({ content, clickHandler }) => {
{content} diff --git a/src/client/components/shared/T2Table.tsx b/src/client/components/shared/T2Table.tsx index f098259..702ce34 100644 --- a/src/client/components/shared/T2Table.tsx +++ b/src/client/components/shared/T2Table.tsx @@ -71,7 +71,7 @@ export const T2Table = (tableOptions: T2TableProps): ReactElement => { columns, manualPagination: true, getCoreRowModel: getCoreRowModel(), - pageCount: sourceData.length ?? -1, + pageCount: sourceData?.length ?? -1, state: { pagination, }, diff --git a/yarn.lock b/yarn.lock index 4dbae63..a946bca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1614,6 +1614,11 @@ resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.5.1.tgz#55cc8410abf1003b726324661ce5b0d1c10de258" integrity sha512-CNy5vSwN3fsUStPRLX7fUYojyuzoEMSXPl7zSLJ8TgtRfjv24LOnOWKT2zYwaHZCJGkdyRnTmstR0P+Ah503Gw== +"@graphql-typed-document-node/core@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861" + integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ== + "@humanwhocodes/config-array@^0.11.13": version "0.11.14" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" @@ -6521,6 +6526,18 @@ graphemer@^1.4.0: resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== +graphql-request@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-7.2.0.tgz#af4aa25f27a087dd4fc93a4ff54a0f59c4487269" + integrity sha512-0GR7eQHBFYz372u9lxS16cOtEekFlZYB2qOyq8wDvzRmdRSJ0mgUVX1tzNcIzk3G+4NY+mGtSz411wZdeDF/+A== + dependencies: + "@graphql-typed-document-node/core" "^3.2.0" + +graphql@^16.0.0: + version "16.11.0" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.11.0.tgz#96d17f66370678027fdf59b2d4c20b4efaa8a633" + integrity sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw== + gunzip-maybe@^1.4.2: version "1.4.2" resolved "https://registry.yarnpkg.com/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz#b913564ae3be0eda6f3de36464837a9cd94b98ac"