-
-
+ {/* 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 (
- -
- {download.name}
-
- );
- })}
+ {data.getValue()?.map((download: any, idx: number) => (
+ -
+ {download.name}
+
+ ))}
),
},
- {
- 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 */}
-
-

-
- >
+
+

+
);
+
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"