💅🏼 Prettifying search results

This commit is contained in:
2025-07-22 18:46:25 -04:00
parent 80f0ced0b0
commit 924ffae07e
8 changed files with 407 additions and 377 deletions

View File

@@ -41,6 +41,8 @@
"final-form": "^4.20.2", "final-form": "^4.20.2",
"final-form-arrays": "^3.0.2", "final-form-arrays": "^3.0.2",
"focus-trap-react": "^10.2.3", "focus-trap-react": "^10.2.3",
"graphql": "^16.0.0",
"graphql-request": "^7.2.0",
"history": "^5.3.0", "history": "^5.3.0",
"html-to-text": "^8.1.0", "html-to-text": "^8.1.0",
"i18next": "^23.11.1", "i18next": "^23.11.1",

View File

@@ -266,53 +266,179 @@ export const Search = ({}: ISearchProps): ReactElement => {
</div> </div>
)} )}
{!isEmpty(comicVineSearchResults?.data?.results) ? ( {!isEmpty(comicVineSearchResults?.data?.results) ? (
<div className="mx-auto max-w-screen-xl px-4 py-4 sm:px-6 sm:py-8 lg:px-8"> <div className="mx-auto w-full sm:w-[90vw] md:w-[80vw] lg:w-[70vw] max-w-6xl px-4 py-6">
{comicVineSearchResults.data.results.map((result) => { {comicVineSearchResults.data.results.map((result) => {
return result.resource_type === "issue" ? ( return result.resource_type === "issue" ? (
<div <div
key={result.id} key={result.id}
className="mb-5 dark:bg-slate-400 p-4 rounded-lg" className="relative flex items-start gap-4 py-6 border-b border-slate-300 dark:border-slate-700"
> >
<div className="flex flex-row"> {/* IMAGE */}
<div className="mr-5 min-w-[80px] max-w-[13%]">
<Card <Card
key={result.id} orientation="cover-only"
orientation={"cover-only"} imageUrl={result?.image?.small_url}
imageUrl={result.image.small_url}
hasDetails={false} hasDetails={false}
cardContainerStyle={{
width: "120px",
maxWidth: "150px",
}}
/> />
{/* RIGHT-SIDE CONTENT */}
<div className="flex-1 min-w-0">
{/* TITLE */}
<div className="text-base font-semibold text-gray-900 dark:text-white truncate">
{result.volume?.name || <span>No Name</span>}
</div> </div>
<div className="w-3/4">
<div className="text-xl"> {/* SUBMETA */}
{!isEmpty(result.volume.name) ? ( <div className="flex flex-wrap gap-2 mt-2">
result.volume.name {/* Cover Date Token */}
) : (
<span className="is-size-3">No Name</span>
)}
</div>
{result.cover_date && ( {result.cover_date && (
<p> <span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-0.5 rounded-md dark:text-slate-900 dark:bg-slate-400">
<span className="tag is-light">Cover date</span> <span className="pr-1 pt-1">
{dayjs(result.cover_date).format("MMM D, YYYY")} <i className="icon-[solar--calendar-bold-duotone] w-4 h-4"></i>
</p> </span>
<span className="text-xs text-slate-500 dark:text-slate-900">
{dayjs(result.cover_date).format("MMM YYYY")}
</span>
</span>
)} )}
<p className="tag is-warning">{result.id}</p> {/* ID Token */}
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-0.5 rounded-md dark:text-slate-900 dark:bg-slate-400">
<span className="pr-1 pt-1">
<i className="icon-[solar--hashtag-bold-duotone] w-4 h-4"></i>
</span>
<span className="text-xs text-slate-500 dark:text-slate-900">
{result.id}
</span>
</span>
</div>
<a href={result.api_detail_url}> {/* LINK */}
<a
href={result.api_detail_url}
className="text-xs text-blue-500 underline mt-1 inline-block break-all"
>
{result.api_detail_url} {result.api_detail_url}
</a> </a>
<p className="text-sm">
{/* DESCRIPTION */}
{result.description && (
<p className="text-sm text-slate-600 dark:text-slate-200 mt-2 line-clamp-3">
{ellipsize(
convert(result.description ?? "", {
baseElements: { selectors: ["p", "div"] },
}),
300,
)}
</p>
)}
{/* CTA BUTTON */}
{result.volume.name ? (
<div className="mt-4 justify-self-end">
<PopoverButton
content={`This will add ${result?.volume?.name} to your wanted list.`}
clickHandler={() =>
addToWantedList({
source: "comicvine",
comicObject: result,
markEntireVolumeWanted: false,
resourceType: "issue",
})
}
/>
</div>
) : null}
</div>
</div>
) : (
result.resource_type === "volume" && (
<div
key={result.id}
className="flex gap-4 py-4 border-b border-slate-300 dark:border-slate-700"
>
{/* LEFT COLUMN: COVER */}
<Card
orientation="cover-only"
imageUrl={result.image.small_url}
hasDetails={false}
cardContainerStyle={{
width: "120px",
maxWidth: "150px",
}}
/>
{/* RIGHT COLUMN */}
<div className="flex-1 min-w-0 flex flex-col">
{/* TITLE */}
<div className="text-lg font-bold text-gray-900 dark:text-white">
{result.name || <span>No Name</span>}
{result.start_year && <> ({result.start_year})</>}
</div>
{/* TOKENS */}
<div className="flex flex-wrap gap-2 mt-2">
{/* ISSUE COUNT */}
{result.count_of_issues && (
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-0.5 rounded-md dark:text-slate-900 dark:bg-slate-400">
<span className="pr-1 pt-1">
<i className="icon-[solar--documents-minimalistic-bold-duotone] w-4 h-4" />
</span>
<span>
{t("issueWithCount", {
count: result.count_of_issues,
})}
</span>
</span>
)}
{/* FORMAT DETECTED */}
{result.description &&
!isEmpty(detectIssueTypes(result.description)) && (
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-0.5 rounded-md dark:text-slate-900 dark:bg-slate-400">
<span className="pr-1 pt-1">
<i className="icon-[solar--book-2-line-duotone] w-4 h-4" />
</span>
<span>
{
detectIssueTypes(result.description)
.displayName
}
</span>
</span>
)}
{/* ID */}
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-0.5 rounded-md dark:text-slate-900 dark:bg-slate-400">
<span className="pr-1 pt-1">
<i className="icon-[solar--hashtag-bold-duotone] w-4 h-4" />
</span>
<span>{result.id}</span>
</span>
</div>
{/* LINK */}
<a
href={result.api_detail_url}
className="text-sm text-blue-500 underline mt-2 break-all"
>
{result.api_detail_url}
</a>
{/* DESCRIPTION */}
{result.description && (
<p className="text-sm mt-2 text-slate-700 dark:text-slate-200 break-words line-clamp-3">
{ellipsize( {ellipsize(
convert(result.description, { convert(result.description, {
baseElements: { baseElements: { selectors: ["p", "div"] },
selectors: ["p", "div"],
},
}), }),
320, 320,
)} )}
</p> </p>
<div className="mt-2"> )}
<PopoverButton <PopoverButton
content={`This will add ${result.volume.name} to your wanted list.`} content={`This will add ${result.volume.name} to your wanted list.`}
clickHandler={() => clickHandler={() =>
@@ -326,115 +452,6 @@ export const Search = ({}: ISearchProps): ReactElement => {
/> />
</div> </div>
</div> </div>
</div>
</div>
) : (
result.resource_type === "volume" && (
<div
key={result.id}
className="mb-5 dark:bg-slate-500 p-4 rounded-lg"
>
<div className="flex flex-row">
<div className="mr-5 min-w-[80px] max-w-[13%]">
<Card
key={result.id}
orientation={"cover-only"}
imageUrl={result.image.small_url}
hasDetails={false}
/>
</div>
<div className="w-3/4">
<div className="text-xl">
{!isEmpty(result.name) ? (
result.name
) : (
<span className="text-xl">No Name</span>
)}
{result.start_year && <> ({result.start_year})</>}
</div>
<div className="flex flex-row gap-2">
{/* issue count */}
{result.count_of_issues && (
<div className="my-2">
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
<span className="pr-1 pt-1">
<i className="icon-[solar--documents-minimalistic-bold-duotone] w-5 h-5"></i>
</span>
<span className="text-md text-slate-500 dark:text-slate-900">
{t("issueWithCount", {
count: result.count_of_issues,
})}
</span>
</span>
</div>
)}
{/* type: TPB, one-shot, graphic novel etc. */}
{!isNil(result.description) &&
!isUndefined(result.description) && (
<>
{!isEmpty(
detectIssueTypes(result.description),
) && (
<div className="my-2">
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
<span className="pr-1 pt-1">
<i className="icon-[solar--book-2-line-duotone] w-5 h-5"></i>
</span>
<span className="text-md text-slate-500 dark:text-slate-900">
{
detectIssueTypes(result.description)
.displayName
}
</span>
</span>
</div>
)}
</>
)}
</div>
<span className="tag is-warning">{result.id}</span>
<p>
<a href={result.api_detail_url}>
{result.api_detail_url}
</a>
</p>
{/* description */}
<p className="text-sm">
{ellipsize(
convert(result.description, {
baseElements: {
selectors: ["p", "div"],
},
}),
320,
)}
</p>
<div className="mt-2">
<PopoverButton
content={`Adding this volume will add ${t(
"issueWithCount",
{
count: result.count_of_issues,
},
)} to your wanted list.`}
clickHandler={() =>
addToWantedList({
source: "comicvine",
comicObject: result,
markEntireVolumeWanted: true,
resourceType: "volume",
})
}
/>
</div>
</div>
</div>
</div>
) )
); );
})} })}

View File

@@ -1,37 +1,87 @@
import React, { ReactElement, useCallback, useEffect, useMemo } from "react"; import React from "react";
import SearchBar from "../Library/SearchBar"; import { useQuery } from "@tanstack/react-query";
import { gql, GraphQLClient } from "graphql-request";
import T2Table from "../shared/T2Table"; import T2Table from "../shared/T2Table";
import MetadataPanel from "../shared/MetadataPanel"; 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 { * GraphQL client for interfacing with Moleculer Apollo server.
data: wantedComics, */
isSuccess, const client = new GraphQLClient("http://localhost:3000/graphql");
isFetched,
isError,
isLoading,
} = useQuery({
queryFn: async () =>
await axios({
url: `${SEARCH_SERVICE_BASE_URI}/searchIssue`,
method: "POST",
data: {
query: {},
pagination: { /**
size: 25, * GraphQL query to fetch wanted comics.
from: 0, */
}, const WANTED_COMICS_QUERY = gql`
type: "wanted", query {
trigger: "wantedComicsPage", 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"], queryKey: ["wantedComics"],
enabled: true, queryFn: async () => {
const res = await client.request<WantedComicsResponse>(
WANTED_COMICS_QUERY,
);
if (!res?.wantedComics?.comics) {
throw new Error("No comics returned");
}
return res.wantedComics;
},
retry: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
}); });
const columnData = [ const columnData = [
{ {
header: "Comic Information", header: "Comic Information",
@@ -40,11 +90,11 @@ export const WantedComics = (props): ReactElement => {
header: "Details", header: "Details",
id: "comicDetails", id: "comicDetails",
minWidth: 350, minWidth: 350,
accessorFn: (data) => data, accessorFn: (data: Comic) => data,
cell: (value) => { cell: (value: any) => {
console.log("ASDASd", value); const row = value.getValue();
const row = value.getValue()._source; console.log("Comic row data:", row);
return row && <MetadataPanel data={row} />; return row ? <MetadataPanel data={row} /> : null;
}, },
}, },
], ],
@@ -55,110 +105,36 @@ export const WantedComics = (props): ReactElement => {
{ {
header: "Files", header: "Files",
align: "right", align: "right",
accessorKey: "_source.acquisition", accessorFn: (row: Comic) =>
cell: (props) => { row?.acquisition?.directconnect?.downloads || [],
const { cell: (props: any) => {
directconnect: { downloads }, const downloads = props.getValue();
} = props.getValue(); return downloads?.length > 0 ? (
return (
<div
style={{
display: "flex",
// flexDirection: "column",
justifyContent: "center",
}}
>
{downloads.length > 0 ? (
<span className="tag is-warning">{downloads.length}</span> <span className="tag is-warning">{downloads.length}</span>
) : null} ) : null;
</div>
);
}, },
}, },
{ {
header: "Download Details", header: "Download Details",
id: "downloadDetails", id: "downloadDetails",
accessorKey: "_source.acquisition", accessorFn: (row: Comic) =>
cell: (data) => ( row?.acquisition?.directconnect?.downloads || [],
cell: (data: any) => (
<ol> <ol>
{data.getValue().directconnect.downloads.map((download, idx) => { {data.getValue()?.map((download: any, idx: number) => (
return (
<li className="is-size-7" key={idx}> <li className="is-size-7" key={idx}>
{download.name} {download.name}
</li> </li>
); ))}
})}
</ol> </ol>
), ),
}, },
{
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 ( return (
<div className=""> <section>
<section className="">
<header className="bg-slate-200 dark:bg-slate-500"> <header className="bg-slate-200 dark:bg-slate-500">
<div className="mx-auto max-w-screen-xl px-2 py-2 sm:px-6 sm:py-8 lg:px-8 lg:py-4"> <div className="mx-auto max-w-screen-xl px-2 py-2 sm:px-6 sm:py-8 lg:px-8 lg:py-4">
<div className="sm:flex sm:items-center sm:justify-between"> <div className="sm:flex sm:items-center sm:justify-between">
@@ -166,7 +142,6 @@ export const WantedComics = (props): ReactElement => {
<h1 className="text-2xl font-bold text-gray-900 dark:text-white sm:text-3xl"> <h1 className="text-2xl font-bold text-gray-900 dark:text-white sm:text-3xl">
Wanted Comics Wanted Comics
</h1> </h1>
<p className="mt-1.5 text-sm text-gray-500 dark:text-white"> <p className="mt-1.5 text-sm text-gray-500 dark:text-white">
Browse through comics you marked as "wanted." Browse through comics you marked as "wanted."
</p> </p>
@@ -174,29 +149,29 @@ export const WantedComics = (props): ReactElement => {
</div> </div>
</div> </div>
</header> </header>
{isSuccess && wantedComics?.data.hits?.hits ? (
<div> {isLoading && (
<div className="library"> <div className="animate-pulse p-4 space-y-4">
<T2Table {Array.from({ length: 5 }).map((_, idx) => (
sourceData={wantedComics?.data.hits.hits} <div
totalPages={wantedComics?.data.hits.hits.length} key={idx}
columns={columnData} className="h-24 bg-slate-300 dark:bg-slate-600 rounded-md"
paginationHandlers={{
nextPage: () => {},
previousPage: () => {},
}}
// rowClickHandler={navigateToComicDetail}
/> />
{/* pagination controls */} ))}
</div> </div>
</div> )}
) : null} {isError && <div>Error fetching wanted comics. {error?.message}</div>}
{isLoading ? <div>Loading...</div> : null} {isSuccess && data?.comics?.length > 0 ? (
{isError ? ( <T2Table
<div>An error occurred while retrieving the pull list.</div> sourceData={data.comics}
totalPages={data.comics.length}
columns={columnData}
paginationHandlers={{}}
/>
) : isSuccess ? (
<div>No comics found.</div>
) : null} ) : null}
</section> </section>
</div>
); );
}; };

View File

@@ -140,14 +140,31 @@ const renderCard = (props: ICardProps): ReactElement => {
); );
case "cover-only": 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 ( return (
<> <div
{/* thumbnail */} className={`rounded-lg overflow-hidden shadow-md bg-white dark:bg-slate-800 ${
<div className="rounded-lg shadow-lg overflow-hidden w-fit h-fit"> props.cardContainerStyle?.height ? "" : "aspect-[2/3]"
<img src={props.imageUrl} /> }`}
style={containerStyle}
>
<img src={props.imageUrl} alt="Comic cover" style={imageStyle} />
</div> </div>
</>
); );
case "card-with-info-panel": case "card-with-info-panel":
return ( return (
<> <>

View File

@@ -1,32 +1,54 @@
import React, { ReactElement } from "react"; import React, { ReactElement } from "react";
import PropTypes from "prop-types";
import ellipsize from "ellipsize"; import ellipsize from "ellipsize";
import prettyBytes from "pretty-bytes"; import prettyBytes from "pretty-bytes";
import { Card } from "../shared/Carda"; import { Card } from "../shared/Carda";
import { convert } from "html-to-text"; import { convert } from "html-to-text";
import { determineCoverFile } from "../../shared/utils/metadata.utils"; import { determineCoverFile } from "../../shared/utils/metadata.utils";
import { find, isUndefined } from "lodash"; import { find, isUndefined } from "lodash";
import { o } from "react-router/dist/development/fog-of-war-BLArG-qZ";
interface IMetadatPanelProps { /**
value: any; * Props for the MetadataPanel component.
children: any; */
imageStyle: any; interface MetadataPanelProps {
titleStyle: any; /**
tagsStyle: any; * Comic metadata object passed into the panel.
containerStyle: any; */
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 { const {
rawFileDetails, rawFileDetails,
inferredMetadata, inferredMetadata,
sourcedMetadata: { comicvine, locg }, sourcedMetadata: { comicvine, locg },
} = props.data; } = props.data;
const { issueName, url, objectReference } = determineCoverFile({ const { issueName, url, objectReference } = determineCoverFile({
comicvine, comicvine,
locg, locg,
rawFileDetails, rawFileDetails,
}); });
const metadataContentPanel = [ const metadataContentPanel = [
{ {
name: "rawFileDetails", name: "rawFileDetails",
@@ -43,48 +65,29 @@ export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => {
</span> </span>
</dd> </dd>
{/* Issue number */}
{inferredMetadata.issue.number && ( {inferredMetadata.issue.number && (
<dd className="my-2"> <dd className="my-2">
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400"> <span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
<span className="pr-1 pt-1"> <i className="icon-[solar--hashtag-outline] w-3.5 h-3.5 pr-1 pt-1"></i>
<i className="icon-[solar--hashtag-outline] w-3.5 h-3.5"></i> <span>{inferredMetadata.issue.number}</span>
</span>
<span className="text-md text-slate-900 dark:text-slate-900">
{inferredMetadata.issue.number}
</span>
</span> </span>
</dd> </dd>
)} )}
<dd className="flex flex-row gap-2 w-max">
{/* File extension */}
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
<span className="pr-1 pt-1">
<i className="icon-[solar--zip-file-bold-duotone] w-5 h-5"></i>
</span>
<span className="text-md text-slate-500 dark:text-slate-900"> <dd className="flex flex-row gap-2 w-max">
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
<i className="icon-[solar--zip-file-bold-duotone] w-5 h-5 pr-1 pt-1" />
{rawFileDetails.mimeType} {rawFileDetails.mimeType}
</span> </span>
</span>
{/* size */}
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400"> <span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
<span className="pr-1 pt-1"> <i className="icon-[solar--mirror-right-bold-duotone] w-5 h-5 pr-1 pt-1" />
<i className="icon-[solar--mirror-right-bold-duotone] w-5 h-5"></i>
</span>
<span className="text-md text-slate-500 dark:text-slate-900">
{prettyBytes(rawFileDetails.fileSize)} {prettyBytes(rawFileDetails.fileSize)}
</span> </span>
</span>
{/* Uncompressed version available? */}
{rawFileDetails.archive?.uncompressed && ( {rawFileDetails.archive?.uncompressed && (
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs px-2 rounded-md dark:text-slate-900 dark:bg-slate-400"> <span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
<span className="pr-1 pt-1"> <i className="icon-[solar--bookmark-bold-duotone] w-3.5 h-3.5 pr-1 pt-1" />
<i className="icon-[solar--bookmark-bold-duotone] w-3.5 h-3.5"></i>
</span>
</span> </span>
)} )}
</dd> </dd>
@@ -94,9 +97,11 @@ export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => {
{ {
name: "comicvine", name: "comicvine",
content: () => content: () => {
!isUndefined(comicvine) && console.log("comicvine:", comicvine);
!isUndefined(comicvine.volumeInformation) && ( console.log("volumeInformation:", comicvine?.volumeInformation);
return (
!isUndefined(comicvine?.volumeInformation) && (
<dl> <dl>
<dt> <dt>
<h6 <h6
@@ -117,10 +122,8 @@ export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => {
<dd className="is-size-7"> <dd className="is-size-7">
<span> <span>
{ellipsize( {ellipsize(
convert(comicvine.description, { convert(comicvine.description || "", {
baseElements: { baseElements: { selectors: ["p"] },
selectors: ["p"],
},
}), }),
120, 120,
)} )}
@@ -131,12 +134,14 @@ export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => {
{comicvine.volumeInformation.start_year} {comicvine.volumeInformation.start_year}
</span> </span>
{comicvine.volumeInformation.count_of_issues} {comicvine.volumeInformation.count_of_issues}
ComicVine ID ComicVine ID: {comicvine.id}
{comicvine.id}
</dd> </dd>
</dl> </dl>
), )
);
}, },
},
{ {
name: "locg", name: "locg",
content: () => ( content: () => (
@@ -147,23 +152,22 @@ export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => {
</h6> </h6>
</dt> </dt>
<dd className="is-size-7"> <dd className="is-size-7">
<span>{ellipsize(locg.description, 120)}</span> <span>{ellipsize(locg?.description || "", 120)}</span>
</dd> </dd>
<dd className="is-size-7 mt-2"> <dd className="is-size-7 mt-2">
<div className="field is-grouped is-grouped-multiline"> <div className="field is-grouped is-grouped-multiline">
<div className="control"> <div className="control">
<span className="tags"> <span className="tags">
<span className="tag is-success is-light has-text-weight-semibold"> <span className="tag is-success is-light has-text-weight-semibold">
{locg.price} {locg?.price}
</span> </span>
<span className="tag is-success is-light">{locg.pulls}</span> <span className="tag is-success is-light">{locg?.pulls}</span>
</span> </span>
</div> </div>
<div className="control"> <div className="control">
<div className="tags has-addons"> <div className="tags has-addons">
<span className="tag is-primary is-light">rating</span> <span className="tag is-primary is-light">rating</span>
<span className="tag is-info is-light">{locg.rating}</span> <span className="tag is-info is-light">{locg?.rating}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -173,20 +177,18 @@ export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => {
}, },
]; ];
// Find the panel to display
const metadataPanel = find(metadataContentPanel, { const metadataPanel = find(metadataContentPanel, {
name: objectReference, name: objectReference,
}); });
return ( return (
<div className="flex gap-5 my-3"> <div className="flex gap-5 my-3">
<Card <Card
imageUrl={url} imageUrl={url}
orientation={"cover-only"} orientation="cover-only"
hasDetails={false} hasDetails={false}
imageStyle={props.imageStyle} imageStyle={props.imageStyle}
/> />
<div>{metadataPanel.content()}</div> <div>{metadataPanel?.content()}</div>
</div> </div>
); );
}; };

View File

@@ -22,7 +22,7 @@ const PopoverButton = ({ content, clickHandler }) => {
onFocus={() => setIsVisible(true)} onFocus={() => setIsVisible(true)}
onBlur={() => setIsVisible(false)} onBlur={() => setIsVisible(false)}
aria-describedby="popover" 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} onClick={clickHandler}
> >
<i className="icon-[solar--add-square-bold-duotone] w-6 h-6 mr-2"></i>{" "} <i className="icon-[solar--add-square-bold-duotone] w-6 h-6 mr-2"></i>{" "}
@@ -32,7 +32,7 @@ const PopoverButton = ({ content, clickHandler }) => {
<div <div
ref={refs.setFloating} // Apply the floating setter directly to the ref prop ref={refs.setFloating} // Apply the floating setter directly to the ref prop
style={floatingStyles} style={floatingStyles}
className="text-sm bg-slate-400 p-2 rounded-md" className="text-xs bg-slate-400 p-1.5 rounded-md"
role="tooltip" role="tooltip"
> >
{content} {content}

View File

@@ -71,7 +71,7 @@ export const T2Table = (tableOptions: T2TableProps): ReactElement => {
columns, columns,
manualPagination: true, manualPagination: true,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
pageCount: sourceData.length ?? -1, pageCount: sourceData?.length ?? -1,
state: { state: {
pagination, pagination,
}, },

View File

@@ -1614,6 +1614,11 @@
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.5.1.tgz#55cc8410abf1003b726324661ce5b0d1c10de258" resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.5.1.tgz#55cc8410abf1003b726324661ce5b0d1c10de258"
integrity sha512-CNy5vSwN3fsUStPRLX7fUYojyuzoEMSXPl7zSLJ8TgtRfjv24LOnOWKT2zYwaHZCJGkdyRnTmstR0P+Ah503Gw== 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": "@humanwhocodes/config-array@^0.11.13":
version "0.11.14" version "0.11.14"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" 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" resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== 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: gunzip-maybe@^1.4.2:
version "1.4.2" version "1.4.2"
resolved "https://registry.yarnpkg.com/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz#b913564ae3be0eda6f3de36464837a9cd94b98ac" resolved "https://registry.yarnpkg.com/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz#b913564ae3be0eda6f3de36464837a9cd94b98ac"