🐘 Massive refactor for graphql changes

This commit is contained in:
2026-03-04 23:42:50 -05:00
parent 4b8d7b5905
commit 74c0d6513c
46 changed files with 10254 additions and 652 deletions

View File

@@ -1,83 +1,70 @@
import React, { ReactElement } from "react";
import ZeroState from "./ZeroState";
import { RecentlyImported } from "./RecentlyImported";
import { WantedComicsList } from "./WantedComicsList";
import { VolumeGroups } from "./VolumeGroups";
import { LibraryStatistics } from "./LibraryStatistics";
import { PullList } from "./PullList";
import { useQuery } from "@tanstack/react-query";
import axios from "axios";
import { LIBRARY_SERVICE_BASE_URI } from "../../constants/endpoints";
import {
useGetRecentComicsQuery,
useGetWantedComicsQuery,
useGetVolumeGroupsQuery,
useGetLibraryStatisticsQuery
} from "../../graphql/generated";
export const Dashboard = (): ReactElement => {
const { data: recentComics } = useQuery({
queryFn: async () =>
await axios({
url: `${LIBRARY_SERVICE_BASE_URI}/getComicBooks`,
method: "POST",
data: {
paginationOptions: {
page: 0,
limit: 5,
sort: { updatedAt: "-1" },
},
predicate: {
wanted: { $exists: false },
},
comicStatus: "recent",
},
}),
queryKey: ["recentComics"],
});
// Wanted Comics
const { data: wantedComics } = useQuery({
queryFn: async () =>
await axios({
url: `${LIBRARY_SERVICE_BASE_URI}/getComicBooks`,
method: "POST",
data: {
paginationOptions: {
page: 0,
limit: 5,
sort: { updatedAt: "-1" },
},
predicate: {
wanted: { $exists: true, $ne: null },
},
},
}),
queryKey: ["wantedComics"],
});
const { data: volumeGroups } = useQuery({
queryFn: async () =>
await axios({
url: `${LIBRARY_SERVICE_BASE_URI}/getComicBookGroups`,
method: "GET",
}),
queryKey: ["volumeGroups"],
});
// Use GraphQL for recent comics
const { data: recentComicsData, error: recentComicsError } = useGetRecentComicsQuery(
{ limit: 5 },
{ refetchOnWindowFocus: false }
);
const { data: statistics } = useQuery({
queryFn: async () =>
await axios({
url: `${LIBRARY_SERVICE_BASE_URI}/libraryStatistics`,
method: "GET",
}),
queryKey: ["libraryStatistics"],
});
// Wanted Comics - using GraphQL
const { data: wantedComicsData, error: wantedComicsError } = useGetWantedComicsQuery(
{
paginationOptions: {
page: 1,
limit: 5,
sort: '{"updatedAt": -1}'
},
predicate: '{"acquisition.source.wanted": true}'
},
{
refetchOnWindowFocus: false,
retry: false
}
);
// Volume Groups - using GraphQL
const { data: volumeGroupsData, error: volumeGroupsError } = useGetVolumeGroupsQuery(
undefined,
{ refetchOnWindowFocus: false }
);
// Library Statistics - using GraphQL
const { data: statisticsData, error: statisticsError } = useGetLibraryStatisticsQuery(
undefined,
{
refetchOnWindowFocus: false,
retry: false
}
);
const recentComics = recentComicsData?.comics?.comics || [];
const wantedComics = !wantedComicsError ? (wantedComicsData?.getComicBooks?.docs || []) : [];
const volumeGroups = volumeGroupsData?.getComicBookGroups || [];
const statistics = !statisticsError ? statisticsData?.getLibraryStatistics : undefined;
return (
<>
<div className="mx-auto max-w-screen-xl px-4 py-4 sm:px-6 sm:py-8 lg:px-8">
<PullList />
{recentComics && <RecentlyImported comics={recentComics?.data.docs} />}
{recentComics.length > 0 && <RecentlyImported comics={recentComics} />}
{/* Wanted comics */}
<WantedComicsList comics={wantedComics?.data?.docs} />
<WantedComicsList comics={wantedComics} />
{/* Library Statistics */}
{statistics && <LibraryStatistics stats={statistics?.data} />}
{statistics && <LibraryStatistics stats={statistics} />}
{/* Volume groups */}
<VolumeGroups volumeGroups={volumeGroups?.data} />
<VolumeGroups volumeGroups={volumeGroups} />
</div>
</>
);

View File

@@ -1,10 +1,14 @@
import React, { ReactElement, useEffect } from "react";
import prettyBytes from "pretty-bytes";
import React, { ReactElement } from "react";
import { isEmpty, isUndefined, map } from "lodash";
import Header from "../shared/Header";
import { GetLibraryStatisticsQuery } from "../../graphql/generated";
type LibraryStatisticsProps = {
stats: GetLibraryStatisticsQuery['getLibraryStatistics'];
};
export const LibraryStatistics = (
props: ILibraryStatisticsProps,
props: LibraryStatisticsProps,
): ReactElement => {
const { stats } = props;
return (
@@ -24,29 +28,30 @@ export const LibraryStatistics = (
<dd className="text-3xl text-green-600 md:text-5xl">
{props.stats.totalDocuments} files
</dd>
<dd>
<span className="text-2xl text-green-600">
{props.stats.comicDirectorySize &&
prettyBytes(props.stats.comicDirectorySize)}
</span>
</dd>
{props.stats.comicDirectorySize?.fileCount && (
<dd>
<span className="text-2xl text-green-600">
{props.stats.comicDirectorySize.fileCount} comic files
</span>
</dd>
)}
</div>
{/* comicinfo and comicvine tagged issues */}
<div className="flex flex-col gap-4">
{!isUndefined(props.stats.statistics) &&
!isEmpty(props.stats.statistics[0].issues) && (
!isEmpty(props.stats.statistics?.[0]?.issues) && (
<div className="flex flex-col h-fit rounded-lg bg-green-100 dark:bg-green-200 px-4 py-3 text-center">
<span className="text-xl">
{props.stats.statistics[0].issues.length}
{props.stats.statistics?.[0]?.issues?.length || 0}
</span>{" "}
tagged with ComicVine
</div>
)}
{!isUndefined(props.stats.statistics) &&
!isEmpty(props.stats.statistics[0].issuesWithComicInfoXML) && (
!isEmpty(props.stats.statistics?.[0]?.issuesWithComicInfoXML) && (
<div className="flex flex-col h-fit rounded-lg bg-green-100 dark:bg-green-200 px-4 py-3 text-center">
<span className="text-xl">
{props.stats.statistics[0].issuesWithComicInfoXML.length}
{props.stats.statistics?.[0]?.issuesWithComicInfoXML?.length || 0}
</span>{" "}
<span className="tag is-warning has-text-weight-bold mr-2 ml-1">
with ComicInfo.xml
@@ -57,14 +62,14 @@ export const LibraryStatistics = (
<div className="">
{!isUndefined(props.stats.statistics) &&
!isEmpty(props.stats.statistics[0].fileTypes) &&
map(props.stats.statistics[0].fileTypes, (fileType, idx) => {
!isEmpty(props.stats.statistics?.[0]?.fileTypes) &&
map(props.stats.statistics?.[0]?.fileTypes, (fileType, idx) => {
return (
<span
key={idx}
className="flex flex-col mb-4 h-fit text-xl rounded-lg bg-green-100 dark:bg-green-200 px-4 py-3 text-center"
>
{fileType.data.length} {fileType._id}
{fileType.data.length} {fileType.id}
</span>
);
})}
@@ -75,20 +80,20 @@ export const LibraryStatistics = (
{/* publisher with most issues */}
{!isUndefined(props.stats.statistics) &&
!isEmpty(
props.stats.statistics[0].publisherWithMostComicsInLibrary[0],
props.stats.statistics?.[0]?.publisherWithMostComicsInLibrary?.[0],
) && (
<>
<span className="">
{
props.stats.statistics[0]
.publisherWithMostComicsInLibrary[0]._id
props.stats.statistics?.[0]
?.publisherWithMostComicsInLibrary?.[0]?.id
}
</span>
{" has the most issues "}
<span className="">
{
props.stats.statistics[0]
.publisherWithMostComicsInLibrary[0].count
props.stats.statistics?.[0]
?.publisherWithMostComicsInLibrary?.[0]?.count
}
</span>
</>

View File

@@ -1,31 +1,22 @@
import React, { ReactElement, useState, useCallback } from "react";
import React, { ReactElement, useState } from "react";
import { map } from "lodash";
import Card from "../shared/Carda";
import Header from "../shared/Header";
import { importToDB } from "../../actions/fileops.actions";
import ellipsize from "ellipsize";
import { Link } from "react-router-dom";
import axios from "axios";
import rateLimiter from "axios-rate-limit";
import { setupCache } from "axios-cache-interceptor";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import useEmblaCarousel from "embla-carousel-react";
import { COMICVINE_SERVICE_URI, LIBRARY_SERVICE_BASE_URI } from "../../constants/endpoints";
import { Field, Form } from "react-final-form";
import { LIBRARY_SERVICE_BASE_URI } from "../../constants/endpoints";
import { Form } from "react-final-form";
import DatePickerDialog from "../shared/DatePicker";
import { format } from "date-fns";
import { LocgMetadata, useGetWeeklyPullListQuery } from "../../graphql/generated";
type PullListProps = {
issues: any;
};
interface PullListProps {
issues?: LocgMetadata[];
}
const http = rateLimiter(axios.create(), {
maxRequests: 1,
perMilliseconds: 1000,
maxRPS: 1,
});
const cachedAxios = setupCache(axios);
export const PullList = (): ReactElement => {
const queryClient = useQueryClient();
@@ -44,20 +35,22 @@ export const PullList = (): ReactElement => {
});
const {
data: pullList,
data: pullListData,
refetch,
isSuccess,
isLoading,
isError,
} = useQuery({
queryFn: async (): any =>
await cachedAxios(`${COMICVINE_SERVICE_URI}/getWeeklyPullList`, {
method: "get",
params: { startDate: inputValue, pageSize: "15", currentPage: "1" },
}),
queryKey: ["pullList", inputValue],
} = useGetWeeklyPullListQuery({
input: {
startDate: inputValue,
pageSize: 15,
currentPage: 1,
},
});
// Transform the data to match the old structure
const pullList = pullListData ? { data: pullListData.getWeeklyPullList } : undefined;
const { mutate: addToLibrary } = useMutation({
mutationFn: async ({ sourceName, metadata }: { sourceName: string; metadata: any }) => {
const comicBookMetadata = {
@@ -146,7 +139,7 @@ export const PullList = (): ReactElement => {
{isSuccess && !isLoading && (
<div className="overflow-hidden" ref={emblaRef}>
<div className="flex">
{map(pullList?.data.result, (issue, idx) => {
{map(pullList?.data.result, (issue: LocgMetadata, idx: number) => {
return (
<div
key={idx}
@@ -154,13 +147,13 @@ export const PullList = (): ReactElement => {
>
<Card
orientation={"vertical-2"}
imageUrl={issue.coverImageUrl}
imageUrl={issue.cover || undefined}
hasDetails
title={ellipsize(issue.issueName, 25)}
title={ellipsize(issue.name || 'Unknown', 25)}
>
<div className="px-1">
<span className="inline-flex mb-2 items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-1 rounded-md dark:text-slate-900 dark:bg-slate-400">
{issue.publicationDate}
{issue.publisher || 'Unknown Publisher'}
</span>
<div className="flex flex-row justify-end">
<button

View File

@@ -4,23 +4,19 @@ import { Link } from "react-router-dom";
import ellipsize from "ellipsize";
import { isEmpty, isNil, isUndefined, map } from "lodash";
import { detectIssueTypes } from "../../shared/utils/tradepaperback.utils";
import {
determineCoverFile,
determineExternalMetadata,
} from "../../shared/utils/metadata.utils";
import { determineCoverFile } from "../../shared/utils/metadata.utils";
import { LIBRARY_SERVICE_HOST } from "../../constants/endpoints";
import Header from "../shared/Header";
import useEmblaCarousel from "embla-carousel-react";
import { GetRecentComicsQuery } from "../../graphql/generated";
type RecentlyImportedProps = {
comics: any;
comics: GetRecentComicsQuery['comics']['comics'];
};
export const RecentlyImported = (
comics: RecentlyImportedProps,
{ comics }: RecentlyImportedProps,
): ReactElement => {
console.log(comics);
// embla carousel
const [emblaRef, emblaApi] = useEmblaCarousel({
loop: false,
@@ -39,31 +35,30 @@ export const RecentlyImported = (
<div className="-mr-10 sm:-mr-17 lg:-mr-29 xl:-mr-36 2xl:-mr-42 mt-3">
<div className="overflow-hidden" ref={emblaRef}>
<div className="flex">
{comics?.comics.map(
(
{
_id,
{comics?.map((comic, idx) => {
const {
id,
rawFileDetails,
sourcedMetadata: { comicvine, comicInfo, locg },
sourcedMetadata,
canonicalMetadata,
inferredMetadata,
wanted: { source } = {},
},
idx,
) => {
} = comic;
// Parse sourced metadata (GraphQL returns as strings)
const comicvine = typeof sourcedMetadata?.comicvine === 'string'
? JSON.parse(sourcedMetadata.comicvine)
: sourcedMetadata?.comicvine;
const comicInfo = typeof sourcedMetadata?.comicInfo === 'string'
? JSON.parse(sourcedMetadata.comicInfo)
: sourcedMetadata?.comicInfo;
const locg = sourcedMetadata?.locg;
const { issueName, url } = determineCoverFile({
rawFileDetails,
comicvine,
comicInfo,
locg,
});
const { issue, coverURL, icon } = determineExternalMetadata(
source,
{
comicvine,
comicInfo,
locg,
},
);
const isComicVineMetadataAvailable =
!isUndefined(comicvine) &&
!isUndefined(comicvine.volumeInformation);
@@ -77,7 +72,7 @@ export const RecentlyImported = (
<Card
orientation="vertical-2"
imageUrl={url}
title={inferredMetadata.issue.name}
title={inferredMetadata?.issue?.name}
hasDetails
cardState={cardState}
>
@@ -89,7 +84,7 @@ export const RecentlyImported = (
<i className="icon-[solar--hashtag-outline]"></i>
</span>
<span className="text-md text-slate-900">
{inferredMetadata.issue.number}
{inferredMetadata?.issue?.number}
</span>
</span>
{/* File extension */}
@@ -99,7 +94,7 @@ export const RecentlyImported = (
</span>
<span className="text-md text-slate-500 dark:text-slate-900">
{rawFileDetails.extension}
{rawFileDetails?.extension}
</span>
</span>
{/* Uncompressed status */}
@@ -142,8 +137,7 @@ export const RecentlyImported = (
</Card>
</div>
);
},
)}
})}
</div>
</div>
</div>

View File

@@ -5,12 +5,17 @@ import { Link, useNavigate } from "react-router-dom";
import Card from "../shared/Carda";
import Header from "../shared/Header";
import useEmblaCarousel from "embla-carousel-react";
import { GetVolumeGroupsQuery } from "../../graphql/generated";
export const VolumeGroups = (props): ReactElement => {
type VolumeGroupsProps = {
volumeGroups?: GetVolumeGroupsQuery['getComicBookGroups'];
};
export const VolumeGroups = (props: VolumeGroupsProps): ReactElement => {
// Till mongo gives us back the deduplicated results with the ObjectId
const deduplicatedGroups = unionBy(props.volumeGroups, "volumes.id");
const navigate = useNavigate();
const navigateToVolumes = (row) => {
const navigateToVolumes = (row: any) => {
navigate(`/volumes/all`);
};
@@ -36,18 +41,18 @@ export const VolumeGroups = (props): ReactElement => {
{map(deduplicatedGroups, (data) => {
return (
<div
key={data._id}
key={data.id}
className="flex-[0_0_200px] min-w-0 sm:flex-[0_0_220px] md:flex-[0_0_240px] lg:flex-[0_0_260px] xl:flex-[0_0_280px] pr-[15px]"
>
<Card
orientation="vertical-2"
imageUrl={data.volumes.image.small_url}
imageUrl={data.volumes?.image?.small_url || undefined}
hasDetails
>
<div className="py-3">
<div className="text-sm">
<Link to={`/volume/details/${data._id}`}>
{ellipsize(data.volumes.name, 48)}
<Link to={`/volume/details/${data.id}`}>
{ellipsize(data.volumes?.name || 'Unknown', 48)}
</Link>
</div>
{/* issue count */}
@@ -57,7 +62,7 @@ export const VolumeGroups = (props): ReactElement => {
</span>
<span className="text-md text-slate-500 dark:text-slate-900">
{data.volumes.count_of_issues} issues
{data.volumes?.count_of_issues || 0} issues
</span>
</span>
</div>

View File

@@ -7,9 +7,10 @@ import { detectIssueTypes } from "../../shared/utils/tradepaperback.utils";
import { determineCoverFile } from "../../shared/utils/metadata.utils";
import Header from "../shared/Header";
import useEmblaCarousel from "embla-carousel-react";
import { GetWantedComicsQuery } from "../../graphql/generated";
type WantedComicsListProps = {
comics: any;
comics?: GetWantedComicsQuery['getComicBooks']['docs'];
};
export const WantedComicsList = ({
@@ -38,12 +39,22 @@ export const WantedComicsList = ({
<div className="flex">
{map(
comics,
({
_id,
rawFileDetails,
sourcedMetadata: { comicvine, comicInfo, locg },
wanted,
}) => {
(comic) => {
const {
id,
rawFileDetails,
sourcedMetadata,
} = comic;
// Parse sourced metadata (GraphQL returns as strings)
const comicvine = typeof sourcedMetadata?.comicvine === 'string'
? JSON.parse(sourcedMetadata.comicvine)
: sourcedMetadata?.comicvine;
const comicInfo = typeof sourcedMetadata?.comicInfo === 'string'
? JSON.parse(sourcedMetadata.comicInfo)
: sourcedMetadata?.comicInfo;
const locg = sourcedMetadata?.locg;
const isComicBookMetadataAvailable = !isUndefined(comicvine);
const consolidatedComicMetadata = {
rawFileDetails,
@@ -58,14 +69,14 @@ export const WantedComicsList = ({
publisher = null,
} = determineCoverFile(consolidatedComicMetadata);
const titleElement = (
<Link to={"/comic/details/" + _id}>
<Link to={"/comic/details/" + id}>
{ellipsize(issueName, 20)}
<p>{publisher}</p>
</Link>
);
return (
<div
key={_id}
key={id}
className="flex-[0_0_200px] min-w-0 sm:flex-[0_0_220px] md:flex-[0_0_240px] lg:flex-[0_0_260px] xl:flex-[0_0_280px] pr-[15px]"
>
<Card
@@ -79,7 +90,7 @@ export const WantedComicsList = ({
<div className="flex flex-row gap-2">
{/* Issue type */}
{isComicBookMetadataAvailable &&
!isNil(detectIssueTypes(comicvine.description)) ? (
!isNil(detectIssueTypes(comicvine?.description)) ? (
<div className="my-2">
<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">
@@ -88,29 +99,14 @@ export const WantedComicsList = ({
<span className="text-md text-slate-500 dark:text-slate-900">
{
detectIssueTypes(comicvine.description)
.displayName
detectIssueTypes(comicvine?.description)
?.displayName
}
</span>
</span>
</div>
) : null}
{/* issues marked as wanted, part of this volume */}
{wanted?.markEntireVolumeWanted ? (
<div className="text-sm">sagla volume pahije</div>
) : (
<div className="my-2">
<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-bold-duotone] w-5 h-5"></i>
</span>
<span className="text-md text-slate-500 dark:text-slate-900">
{wanted.issues.length}
</span>
</span>
</div>
)}
{/* Wanted comics - info not available in current GraphQL query */}
</div>
{/* comicVine metadata presence */}
{isComicBookMetadataAvailable && (