🐘 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

16
codegen.yml Normal file
View File

@@ -0,0 +1,16 @@
schema: http://localhost:3000/graphql
documents: 'src/client/graphql/**/*.graphql'
generates:
src/client/graphql/generated.ts:
plugins:
- typescript
- typescript-operations
- typescript-react-query
config:
fetcher:
func: './fetcher#fetcher'
isReactHook: false
exposeFetcher: true
exposeQueryKeys: true
addInfiniteQuery: true
reactQueryVersion: 5

4601
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,11 +13,14 @@
"test:watch": "jest --watch", "test:watch": "jest --watch",
"test:coverage": "jest --coverage", "test:coverage": "jest --coverage",
"storybook": "storybook dev -p 6006", "storybook": "storybook dev -p 6006",
"build-storybook": "storybook build" "build-storybook": "storybook build",
"codegen": "graphql-codegen --config codegen.yml",
"codegen:watch": "graphql-codegen --config codegen.yml --watch"
}, },
"author": "Rishi Ghan", "author": "Rishi Ghan",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@apollo/client": "^4.1.6",
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
@@ -45,6 +48,7 @@
"final-form": "^5.0.0", "final-form": "^5.0.0",
"final-form-arrays": "^4.0.0", "final-form-arrays": "^4.0.0",
"focus-trap-react": "^12.0.0", "focus-trap-react": "^12.0.0",
"graphql": "^16.13.1",
"history": "^5.3.0", "history": "^5.3.0",
"html-to-text": "^9.0.5", "html-to-text": "^9.0.5",
"i18next": "^25.8.13", "i18next": "^25.8.13",
@@ -74,6 +78,7 @@
"react-sliding-pane": "^7.3.0", "react-sliding-pane": "^7.3.0",
"react-textarea-autosize": "^8.5.9", "react-textarea-autosize": "^8.5.9",
"react-toastify": "^11.0.5", "react-toastify": "^11.0.5",
"rxjs": "^7.8.2",
"socket.io-client": "^4.8.3", "socket.io-client": "^4.8.3",
"styled-components": "^6.3.11", "styled-components": "^6.3.11",
"threetwo-ui-typings": "^1.0.14", "threetwo-ui-typings": "^1.0.14",
@@ -83,6 +88,10 @@
"zustand": "^5.0.11" "zustand": "^5.0.11"
}, },
"devDependencies": { "devDependencies": {
"@graphql-codegen/cli": "^6.1.2",
"@graphql-codegen/typescript": "^5.0.8",
"@graphql-codegen/typescript-operations": "^5.0.8",
"@graphql-codegen/typescript-react-query": "^6.1.2",
"@iconify-json/solar": "^1.2.5", "@iconify-json/solar": "^1.2.5",
"@iconify/json": "^2.2.443", "@iconify/json": "^2.2.443",
"@iconify/tailwind": "^1.2.0", "@iconify/tailwind": "^1.2.0",

View File

@@ -55,7 +55,6 @@ export const toggleAirDCPPSocketConnectionStatus =
break; break;
default: default:
console.log("Can't set AirDC++ socket status.");
break; break;
} }
}; };

View File

@@ -43,7 +43,7 @@ export const getWeeklyPullList = (options) => async (dispatch) => {
}); });
}); });
} catch (error) { } catch (error) {
console.log(error); // Error handling could be added here if needed
} }
}; };
@@ -73,10 +73,9 @@ export const comicinfoAPICall = (options) => async (dispatch) => {
break; break;
default: default:
console.log("Could not complete request."); break;
} }
} catch (error) { } catch (error) {
console.log(error);
dispatch({ dispatch({
type: CV_API_GENERIC_FAILURE, type: CV_API_GENERIC_FAILURE,
error, error,
@@ -99,7 +98,6 @@ export const getIssuesForSeries =
comicObjectID, comicObjectID,
}, },
}); });
console.log(issues);
dispatch({ dispatch({
type: CV_ISSUES_FOR_VOLUME_IN_LIBRARY_SUCCESS, type: CV_ISSUES_FOR_VOLUME_IN_LIBRARY_SUCCESS,
issues: issues.data.results, issues: issues.data.results,

View File

@@ -34,7 +34,6 @@ import {
LS_SET_QUEUE_STATUS, LS_SET_QUEUE_STATUS,
LS_IMPORT_JOB_STATISTICS_FETCHED, LS_IMPORT_JOB_STATISTICS_FETCHED,
} from "../constants/action-types"; } from "../constants/action-types";
import { success } from "react-notification-system-redux";
import { isNil } from "lodash"; import { isNil } from "lodash";
@@ -151,7 +150,7 @@ export const getComicBooks = (options) => async (dispatch) => {
}); });
break; break;
default: default:
console.log("Unrecognized comic status."); break;
} }
}; };
@@ -219,12 +218,11 @@ export const fetchVolumeGroups = () => async (dispatch) => {
data: response.data, data: response.data,
}); });
} catch (error) { } catch (error) {
console.log(error); // Error handling could be added here if needed
} }
}; };
export const fetchComicVineMatches = export const fetchComicVineMatches =
(searchPayload, issueSearchQuery, seriesSearchQuery?) => async (dispatch) => { (searchPayload, issueSearchQuery, seriesSearchQuery?) => async (dispatch) => {
console.log(issueSearchQuery);
try { try {
dispatch({ dispatch({
type: CV_API_CALL_IN_PROGRESS, type: CV_API_CALL_IN_PROGRESS,
@@ -273,7 +271,7 @@ export const fetchComicVineMatches =
}); });
}); });
} catch (error) { } catch (error) {
console.log(error); // Error handling could be added here if needed
} }
dispatch({ dispatch({

View File

@@ -7,8 +7,6 @@ export const fetchMetronResource = async (options) => {
`${METRON_SERVICE_URI}/fetchResource`, `${METRON_SERVICE_URI}/fetchResource`,
options, options,
); );
console.log(metronResourceResults);
console.log("has more? ", !isNil(metronResourceResults.data.next));
const results = metronResourceResults.data.results.map((result) => { const results = metronResourceResults.data.results.map((result) => {
return { return {
label: result.name || result.__str__, label: result.name || result.__str__,

View File

@@ -1,9 +1,11 @@
import React, { ReactElement, useEffect } from "react"; import React, { ReactElement, useEffect } from "react";
import { Outlet } from "react-router-dom"; import { Outlet } from "react-router-dom";
import { ApolloProvider } from "@apollo/client/react";
import { Navbar2 } from "./shared/Navbar2"; import { Navbar2 } from "./shared/Navbar2";
import { ToastContainer } from "react-toastify"; import { ToastContainer } from "react-toastify";
import "../assets/scss/App.css"; import "../assets/scss/App.css";
import { useStore } from "../store"; import { useStore } from "../store";
import { apolloClient } from "../graphql/client";
export const App = (): ReactElement => { export const App = (): ReactElement => {
useEffect(() => { useEffect(() => {
@@ -11,11 +13,11 @@ export const App = (): ReactElement => {
}, []); }, []);
return ( return (
<> <ApolloProvider client={apolloClient}>
<Navbar2 /> <Navbar2 />
<Outlet /> <Outlet />
<ToastContainer stacked hideProgressBar /> <ToastContainer stacked hideProgressBar />
</> </ApolloProvider>
); );
}; };

View File

@@ -160,7 +160,9 @@ export const AcquisitionPanel = (
type, type,
config, config,
}, },
(data: any) => console.log(data), (data: any) => {
// Download initiated
},
); );
}; };

View File

@@ -10,6 +10,7 @@ import "react-sliding-pane/dist/react-sliding-pane.css";
import SlidingPane from "react-sliding-pane"; import SlidingPane from "react-sliding-pane";
import { determineCoverFile } from "../../shared/utils/metadata.utils"; import { determineCoverFile } from "../../shared/utils/metadata.utils";
import { styled } from "styled-components"; import { styled } from "styled-components";
import { RawFileDetails as RawFileDetailsType } from "../../graphql/generated";
// Extracted modules // Extracted modules
import { useComicVineMatching } from "./useComicVineMatching"; import { useComicVineMatching } from "./useComicVineMatching";
@@ -22,45 +23,32 @@ const StyledSlidingPanel = styled(SlidingPane)`
background: #ccc; background: #ccc;
`; `;
interface RawFileDetails { type InferredIssue = {
name: string;
cover?: {
filePath?: string;
};
containedIn?: string;
fileSize?: number;
path?: string;
extension?: string;
mimeType?: string;
[key: string]: any;
}
interface InferredIssue {
name?: string; name?: string;
number?: number; number?: number;
year?: string; year?: string;
subtitle?: string; subtitle?: string;
[key: string]: any; [key: string]: any;
} };
interface ComicVineMetadata { type ComicVineMetadata = {
name?: string; name?: string;
volumeInformation?: any; volumeInformation?: any;
[key: string]: any; [key: string]: any;
} };
interface Acquisition { type Acquisition = {
directconnect?: { directconnect?: {
downloads?: any[]; downloads?: any[];
}; };
torrent?: any[]; torrent?: any[];
[key: string]: any; [key: string]: any;
} };
interface ComicDetailProps { type ComicDetailProps = {
data: { data: {
_id: string; _id: string;
rawFileDetails?: RawFileDetails; rawFileDetails?: RawFileDetailsType;
inferredMetadata: { inferredMetadata: {
issue?: InferredIssue; issue?: InferredIssue;
}; };
@@ -76,7 +64,7 @@ interface ComicDetailProps {
userSettings?: any; userSettings?: any;
queryClient?: any; queryClient?: any;
comicObjectId?: string; comicObjectId?: string;
} };
/** /**
* Component for displaying the metadata for a comic in greater detail. * Component for displaying the metadata for a comic in greater detail.
@@ -117,7 +105,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
}, []); }, []);
const afterOpenModal = useCallback((things: any) => { const afterOpenModal = useCallback((things: any) => {
console.log("kolaveri", things); // Modal opened callback
}, []); }, []);
const closeModal = useCallback(() => { const closeModal = useCallback(() => {
@@ -154,7 +142,6 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
openEditMetadataPanel(); openEditMetadataPanel();
break; break;
default: default:
console.log("No valid action selected.");
break; break;
} }
}; };

View File

@@ -1,9 +1,9 @@
import React, { ReactElement } from "react"; import React, { ReactElement } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { ComicDetail } from "../ComicDetail/ComicDetail"; import { ComicDetail } from "../ComicDetail/ComicDetail";
import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { LIBRARY_SERVICE_BASE_URI } from "../../constants/endpoints"; import { useGetComicByIdQuery } from "../../graphql/generated";
import axios from "axios"; import { adaptGraphQLComicToLegacy } from "../../graphql/adapters/comicAdapter";
export const ComicDetailContainer = (): ReactElement | null => { export const ComicDetailContainer = (): ReactElement | null => {
const { comicObjectId } = useParams<{ comicObjectId: string }>(); const { comicObjectId } = useParams<{ comicObjectId: string }>();
@@ -13,31 +13,28 @@ export const ComicDetailContainer = (): ReactElement | null => {
data: comicBookDetailData, data: comicBookDetailData,
isLoading, isLoading,
isError, isError,
} = useQuery({ } = useGetComicByIdQuery(
queryKey: ["comicBookMetadata", comicObjectId], { id: comicObjectId! },
queryFn: async () => { enabled: !!comicObjectId }
await axios({
url: `${LIBRARY_SERVICE_BASE_URI}/getComicBookById`,
method: "POST",
data: {
id: comicObjectId,
},
}),
});
{
isError && <>Error</>;
}
{
isLoading && <>Loading...</>;
}
return (
comicBookDetailData?.data && (
<ComicDetail
data={comicBookDetailData.data}
queryClient={queryClient}
comicObjectId={comicObjectId}
/>
)
); );
if (isError) {
return <div>Error loading comic details</div>;
}
if (isLoading) {
return <div>Loading...</div>;
}
const adaptedData = comicBookDetailData?.comic
? adaptGraphQLComicToLegacy(comicBookDetailData.comic)
: null;
return adaptedData ? (
<ComicDetail
data={adaptedData}
queryClient={queryClient}
comicObjectId={comicObjectId}
/>
) : null;
}; };

View File

@@ -77,8 +77,6 @@ export const DownloadProgressTick: React.FC<DownloadProgressTickProps> = ({
*/ */
const onDownloadTick = (data: DownloadTickData) => { const onDownloadTick = (data: DownloadTickData) => {
// Compare numeric data.id to string bundleId // Compare numeric data.id to string bundleId
console.log(data.id);
console.log(`bundleId is ${bundleId}`)
if (data.id === parseInt(bundleId, 10)) { if (data.id === parseInt(bundleId, 10)) {
setTick(data); setTick(data);
} }

View File

@@ -1,21 +1,12 @@
import React, { ReactElement } from "react"; import React, { ReactElement, ReactNode } from "react";
import prettyBytes from "pretty-bytes"; import prettyBytes from "pretty-bytes";
import { isEmpty } from "lodash"; import { isEmpty } from "lodash";
import { format, parseISO } from "date-fns"; import { format, parseISO } from "date-fns";
import { RawFileDetails as RawFileDetailsType } from "../../graphql/generated";
interface RawFileDetailsProps { type RawFileDetailsProps = {
data?: { data?: {
rawFileDetails?: { rawFileDetails?: RawFileDetailsType;
containedIn?: string;
name?: string;
fileSize?: number;
path?: string;
extension?: string;
mimeType?: string;
cover?: {
filePath?: string;
};
};
inferredMetadata?: { inferredMetadata?: {
issue?: { issue?: {
year?: string; year?: string;
@@ -27,18 +18,18 @@ interface RawFileDetailsProps {
created_at?: string; created_at?: string;
updated_at?: string; updated_at?: string;
}; };
children?: any; children?: ReactNode;
} };
export const RawFileDetails = (props: RawFileDetailsProps): ReactElement => { export const RawFileDetails = (props: RawFileDetailsProps): ReactElement => {
const { rawFileDetails, inferredMetadata, created_at, updated_at } = const { rawFileDetails, inferredMetadata, created_at, updated_at } =
props.data; props.data || {};
return ( return (
<> <>
<div className="max-w-2xl ml-5"> <div className="max-w-2xl ml-5">
<div className="px-4 sm:px-6"> <div className="px-4 sm:px-6">
<p className="text-gray-500 dark:text-gray-400"> <p className="text-gray-500 dark:text-gray-400">
<span className="text-xl">{rawFileDetails.name}</span> <span className="text-xl">{rawFileDetails?.name}</span>
</p> </p>
</div> </div>
<div className="px-4 py-5 sm:px-6"> <div className="px-4 py-5 sm:px-6">
@@ -48,10 +39,10 @@ export const RawFileDetails = (props: RawFileDetailsProps): ReactElement => {
Raw File Details Raw File Details
</dt> </dt>
<dd className="mt-1 text-sm text-gray-900 dark:text-gray-400"> <dd className="mt-1 text-sm text-gray-900 dark:text-gray-400">
{rawFileDetails.containedIn + {rawFileDetails?.containedIn}
"/" + {"/"}
rawFileDetails.name + {rawFileDetails?.name}
rawFileDetails.extension} {rawFileDetails?.extension}
</dd> </dd>
</div> </div>
<div className="sm:col-span-1"> <div className="sm:col-span-1">
@@ -59,10 +50,10 @@ export const RawFileDetails = (props: RawFileDetailsProps): ReactElement => {
Inferred Issue Metadata Inferred Issue Metadata
</dt> </dt>
<dd className="mt-1 text-sm text-gray-900 dark:text-gray-400"> <dd className="mt-1 text-sm text-gray-900 dark:text-gray-400">
Series Name: {inferredMetadata.issue.name} Series Name: {inferredMetadata?.issue?.name}
{!isEmpty(inferredMetadata.issue.number) ? ( {!isEmpty(inferredMetadata?.issue?.number) ? (
<span className="tag is-primary is-light"> <span className="tag is-primary is-light">
{inferredMetadata.issue.number} {inferredMetadata?.issue?.number}
</span> </span>
) : null} ) : null}
</dd> </dd>
@@ -79,7 +70,7 @@ export const RawFileDetails = (props: RawFileDetailsProps): ReactElement => {
</span> </span>
<span className="text-md text-slate-500 dark:text-slate-900"> <span className="text-md text-slate-500 dark:text-slate-900">
{rawFileDetails.mimeType} {rawFileDetails?.mimeType}
</span> </span>
</span> </span>
</dd> </dd>
@@ -96,7 +87,7 @@ export const RawFileDetails = (props: RawFileDetailsProps): ReactElement => {
</span> </span>
<span className="text-md text-slate-500 dark:text-slate-900"> <span className="text-md text-slate-500 dark:text-slate-900">
{prettyBytes(rawFileDetails.fileSize)} {rawFileDetails?.fileSize ? prettyBytes(rawFileDetails.fileSize) : "N/A"}
</span> </span>
</span> </span>
</dd> </dd>
@@ -106,8 +97,12 @@ export const RawFileDetails = (props: RawFileDetailsProps): ReactElement => {
Import Details Import Details
</dt> </dt>
<dd className="mt-1 text-sm text-gray-900 dark:text-gray-400"> <dd className="mt-1 text-sm text-gray-900 dark:text-gray-400">
{format(parseISO(created_at), "dd MMMM, yyyy")},{" "} {created_at ? (
{format(parseISO(created_at), "h aaaa")} <>
{format(parseISO(created_at), "dd MMMM, yyyy")},{" "}
{format(parseISO(created_at), "h aaaa")}
</>
) : "N/A"}
</dd> </dd>
</div> </div>
<div className="sm:col-span-2"> <div className="sm:col-span-2">

View File

@@ -2,17 +2,18 @@ import React from "react";
import { ComicVineSearchForm } from "./ComicVineSearchForm"; import { ComicVineSearchForm } from "./ComicVineSearchForm";
import { ComicVineMatchPanel } from "./ComicVineMatchPanel"; import { ComicVineMatchPanel } from "./ComicVineMatchPanel";
import { EditMetadataPanel } from "./EditMetadataPanel"; import { EditMetadataPanel } from "./EditMetadataPanel";
import { RawFileDetails } from "../../graphql/generated";
interface InferredIssue { type InferredIssue = {
name?: string; name?: string;
number?: number; number?: number;
year?: string; year?: string;
subtitle?: string; subtitle?: string;
[key: string]: any; [key: string]: any;
} };
interface CVMatchesPanelProps { type CVMatchesPanelProps = {
rawFileDetails: any; rawFileDetails?: RawFileDetails;
inferredMetadata: { inferredMetadata: {
issue?: InferredIssue; issue?: InferredIssue;
}; };
@@ -20,7 +21,7 @@ interface CVMatchesPanelProps {
comicObjectId: string; comicObjectId: string;
queryClient: any; queryClient: any;
onMatchApplied: () => void; onMatchApplied: () => void;
} };
export const CVMatchesPanel: React.FC<CVMatchesPanelProps> = ({ export const CVMatchesPanel: React.FC<CVMatchesPanelProps> = ({
rawFileDetails, rawFileDetails,
@@ -55,9 +56,9 @@ export const CVMatchesPanel: React.FC<CVMatchesPanelProps> = ({
</> </>
); );
interface EditMetadataPanelWrapperProps { type EditMetadataPanelWrapperProps = {
rawFileDetails: any; rawFileDetails?: RawFileDetails;
} };
export const EditMetadataPanelWrapper: React.FC<EditMetadataPanelWrapperProps> = ({ export const EditMetadataPanelWrapper: React.FC<EditMetadataPanelWrapperProps> = ({
rawFileDetails, rawFileDetails,

View File

@@ -77,8 +77,7 @@ export const ArchiveOperations = (props: { data: any }): ReactElement => {
}, },
}); });
} catch (error) { } catch (error) {
console.error("Error fetching uncompressed archive:", error); // Error handling could be added here if needed
// Handle error if necessary
} }
}; };
fetchUncompressedArchive(); fetchUncompressedArchive();

View File

@@ -4,7 +4,6 @@ import prettyBytes from "pretty-bytes";
export const TorrentDownloads = (props) => { export const TorrentDownloads = (props) => {
const { data } = props; const { data } = props;
console.log(Object.values(data));
return ( return (
<> <>
{data.map(({ torrent }) => { {data.map(({ torrent }) => {

View File

@@ -43,7 +43,7 @@ export const TorrentSearchPanel = (props) => {
mutationFn: async (newTorrent) => mutationFn: async (newTorrent) =>
axios.post(`${QBITTORRENT_SERVICE_BASE_URI}/addTorrent`, newTorrent), axios.post(`${QBITTORRENT_SERVICE_BASE_URI}/addTorrent`, newTorrent),
onSuccess: async (data) => { onSuccess: async (data) => {
console.log(data); // Torrent added successfully
}, },
}); });
const searchIndexer = (values) => { const searchIndexer = (values) => {

View File

@@ -3,29 +3,25 @@ import axios from "axios";
import { isNil, isUndefined, isEmpty } from "lodash"; import { isNil, isUndefined, isEmpty } from "lodash";
import { refineQuery } from "filename-parser"; import { refineQuery } from "filename-parser";
import { COMICVINE_SERVICE_URI } from "../../constants/endpoints"; import { COMICVINE_SERVICE_URI } from "../../constants/endpoints";
import { RawFileDetails as RawFileDetailsType } from "../../graphql/generated";
interface ComicVineMatch { type ComicVineMatch = {
score: number; score: number;
[key: string]: any; [key: string]: any;
} };
interface ComicVineSearchQuery { type ComicVineSearchQuery = {
inferredIssueDetails: { inferredIssueDetails: {
name: string; name: string;
[key: string]: any; [key: string]: any;
}; };
[key: string]: any; [key: string]: any;
} };
interface RawFileDetails { type ComicVineMetadata = {
name: string;
[key: string]: any;
}
interface ComicVineMetadata {
name?: string; name?: string;
[key: string]: any; [key: string]: any;
} };
export const useComicVineMatching = () => { export const useComicVineMatching = () => {
const [comicVineMatches, setComicVineMatches] = useState<ComicVineMatch[]>([]); const [comicVineMatches, setComicVineMatches] = useState<ComicVineMatch[]>([]);
@@ -67,18 +63,18 @@ export const useComicVineMatching = () => {
const scoredMatches = matches.sort((a: ComicVineMatch, b: ComicVineMatch) => b.score - a.score); const scoredMatches = matches.sort((a: ComicVineMatch, b: ComicVineMatch) => b.score - a.score);
setComicVineMatches(scoredMatches); setComicVineMatches(scoredMatches);
} catch (err) { } catch (err) {
console.log(err); // Error handling could be added here if needed
} }
}; };
const prepareAndFetchMatches = ( const prepareAndFetchMatches = (
rawFileDetails: RawFileDetails | undefined, rawFileDetails: RawFileDetailsType | undefined,
comicvine: ComicVineMetadata | undefined, comicvine: ComicVineMetadata | undefined,
) => { ) => {
let seriesSearchQuery: ComicVineSearchQuery = {} as ComicVineSearchQuery; let seriesSearchQuery: ComicVineSearchQuery = {} as ComicVineSearchQuery;
let issueSearchQuery: ComicVineSearchQuery = {} as ComicVineSearchQuery; let issueSearchQuery: ComicVineSearchQuery = {} as ComicVineSearchQuery;
if (!isUndefined(rawFileDetails)) { if (!isUndefined(rawFileDetails) && rawFileDetails.name) {
issueSearchQuery = refineQuery(rawFileDetails.name) as ComicVineSearchQuery; issueSearchQuery = refineQuery(rawFileDetails.name) as ComicVineSearchQuery;
} else if (!isEmpty(comicvine) && comicvine?.name) { } else if (!isEmpty(comicvine) && comicvine?.name) {
issueSearchQuery = refineQuery(comicvine.name) as ComicVineSearchQuery; issueSearchQuery = refineQuery(comicvine.name) as ComicVineSearchQuery;

View File

@@ -1,83 +1,70 @@
import React, { ReactElement } from "react"; import React, { ReactElement } from "react";
import ZeroState from "./ZeroState";
import { RecentlyImported } from "./RecentlyImported"; import { RecentlyImported } from "./RecentlyImported";
import { WantedComicsList } from "./WantedComicsList"; import { WantedComicsList } from "./WantedComicsList";
import { VolumeGroups } from "./VolumeGroups"; import { VolumeGroups } from "./VolumeGroups";
import { LibraryStatistics } from "./LibraryStatistics"; import { LibraryStatistics } from "./LibraryStatistics";
import { PullList } from "./PullList"; import { PullList } from "./PullList";
import { useQuery } from "@tanstack/react-query"; import {
import axios from "axios"; useGetRecentComicsQuery,
import { LIBRARY_SERVICE_BASE_URI } from "../../constants/endpoints"; useGetWantedComicsQuery,
useGetVolumeGroupsQuery,
useGetLibraryStatisticsQuery
} from "../../graphql/generated";
export const Dashboard = (): ReactElement => { export const Dashboard = (): ReactElement => {
const { data: recentComics } = useQuery({ // Use GraphQL for recent comics
queryFn: async () => const { data: recentComicsData, error: recentComicsError } = useGetRecentComicsQuery(
await axios({ { limit: 5 },
url: `${LIBRARY_SERVICE_BASE_URI}/getComicBooks`, { refetchOnWindowFocus: false }
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"],
});
const { data: statistics } = useQuery({ // Wanted Comics - using GraphQL
queryFn: async () => const { data: wantedComicsData, error: wantedComicsError } = useGetWantedComicsQuery(
await axios({ {
url: `${LIBRARY_SERVICE_BASE_URI}/libraryStatistics`, paginationOptions: {
method: "GET", page: 1,
}), limit: 5,
queryKey: ["libraryStatistics"], 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 ( return (
<> <>
<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 max-w-screen-xl px-4 py-4 sm:px-6 sm:py-8 lg:px-8">
<PullList /> <PullList />
{recentComics && <RecentlyImported comics={recentComics?.data.docs} />} {recentComics.length > 0 && <RecentlyImported comics={recentComics} />}
{/* Wanted comics */} {/* Wanted comics */}
<WantedComicsList comics={wantedComics?.data?.docs} /> <WantedComicsList comics={wantedComics} />
{/* Library Statistics */} {/* Library Statistics */}
{statistics && <LibraryStatistics stats={statistics?.data} />} {statistics && <LibraryStatistics stats={statistics} />}
{/* Volume groups */} {/* Volume groups */}
<VolumeGroups volumeGroups={volumeGroups?.data} /> <VolumeGroups volumeGroups={volumeGroups} />
</div> </div>
</> </>
); );

View File

@@ -1,10 +1,14 @@
import React, { ReactElement, useEffect } from "react"; import React, { ReactElement } from "react";
import prettyBytes from "pretty-bytes";
import { isEmpty, isUndefined, map } from "lodash"; import { isEmpty, isUndefined, map } from "lodash";
import Header from "../shared/Header"; import Header from "../shared/Header";
import { GetLibraryStatisticsQuery } from "../../graphql/generated";
type LibraryStatisticsProps = {
stats: GetLibraryStatisticsQuery['getLibraryStatistics'];
};
export const LibraryStatistics = ( export const LibraryStatistics = (
props: ILibraryStatisticsProps, props: LibraryStatisticsProps,
): ReactElement => { ): ReactElement => {
const { stats } = props; const { stats } = props;
return ( return (
@@ -24,29 +28,30 @@ export const LibraryStatistics = (
<dd className="text-3xl text-green-600 md:text-5xl"> <dd className="text-3xl text-green-600 md:text-5xl">
{props.stats.totalDocuments} files {props.stats.totalDocuments} files
</dd> </dd>
<dd> {props.stats.comicDirectorySize?.fileCount && (
<span className="text-2xl text-green-600"> <dd>
{props.stats.comicDirectorySize && <span className="text-2xl text-green-600">
prettyBytes(props.stats.comicDirectorySize)} {props.stats.comicDirectorySize.fileCount} comic files
</span> </span>
</dd> </dd>
)}
</div> </div>
{/* comicinfo and comicvine tagged issues */} {/* comicinfo and comicvine tagged issues */}
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{!isUndefined(props.stats.statistics) && {!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"> <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"> <span className="text-xl">
{props.stats.statistics[0].issues.length} {props.stats.statistics?.[0]?.issues?.length || 0}
</span>{" "} </span>{" "}
tagged with ComicVine tagged with ComicVine
</div> </div>
)} )}
{!isUndefined(props.stats.statistics) && {!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"> <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"> <span className="text-xl">
{props.stats.statistics[0].issuesWithComicInfoXML.length} {props.stats.statistics?.[0]?.issuesWithComicInfoXML?.length || 0}
</span>{" "} </span>{" "}
<span className="tag is-warning has-text-weight-bold mr-2 ml-1"> <span className="tag is-warning has-text-weight-bold mr-2 ml-1">
with ComicInfo.xml with ComicInfo.xml
@@ -57,14 +62,14 @@ export const LibraryStatistics = (
<div className=""> <div className="">
{!isUndefined(props.stats.statistics) && {!isUndefined(props.stats.statistics) &&
!isEmpty(props.stats.statistics[0].fileTypes) && !isEmpty(props.stats.statistics?.[0]?.fileTypes) &&
map(props.stats.statistics[0].fileTypes, (fileType, idx) => { map(props.stats.statistics?.[0]?.fileTypes, (fileType, idx) => {
return ( return (
<span <span
key={idx} 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" 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> </span>
); );
})} })}
@@ -75,20 +80,20 @@ export const LibraryStatistics = (
{/* publisher with most issues */} {/* publisher with most issues */}
{!isUndefined(props.stats.statistics) && {!isUndefined(props.stats.statistics) &&
!isEmpty( !isEmpty(
props.stats.statistics[0].publisherWithMostComicsInLibrary[0], props.stats.statistics?.[0]?.publisherWithMostComicsInLibrary?.[0],
) && ( ) && (
<> <>
<span className=""> <span className="">
{ {
props.stats.statistics[0] props.stats.statistics?.[0]
.publisherWithMostComicsInLibrary[0]._id ?.publisherWithMostComicsInLibrary?.[0]?.id
} }
</span> </span>
{" has the most issues "} {" has the most issues "}
<span className=""> <span className="">
{ {
props.stats.statistics[0] props.stats.statistics?.[0]
.publisherWithMostComicsInLibrary[0].count ?.publisherWithMostComicsInLibrary?.[0]?.count
} }
</span> </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 { map } from "lodash";
import Card from "../shared/Carda"; import Card from "../shared/Carda";
import Header from "../shared/Header"; import Header from "../shared/Header";
import { importToDB } from "../../actions/fileops.actions";
import ellipsize from "ellipsize"; import ellipsize from "ellipsize";
import { Link } from "react-router-dom";
import axios from "axios"; import axios from "axios";
import rateLimiter from "axios-rate-limit"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { setupCache } from "axios-cache-interceptor";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import useEmblaCarousel from "embla-carousel-react"; import useEmblaCarousel from "embla-carousel-react";
import { COMICVINE_SERVICE_URI, LIBRARY_SERVICE_BASE_URI } from "../../constants/endpoints"; import { LIBRARY_SERVICE_BASE_URI } from "../../constants/endpoints";
import { Field, Form } from "react-final-form"; import { Form } from "react-final-form";
import DatePickerDialog from "../shared/DatePicker"; import DatePickerDialog from "../shared/DatePicker";
import { format } from "date-fns"; import { format } from "date-fns";
import { LocgMetadata, useGetWeeklyPullListQuery } from "../../graphql/generated";
type PullListProps = { interface PullListProps {
issues: any; issues?: LocgMetadata[];
}; }
const http = rateLimiter(axios.create(), {
maxRequests: 1,
perMilliseconds: 1000,
maxRPS: 1,
});
const cachedAxios = setupCache(axios);
export const PullList = (): ReactElement => { export const PullList = (): ReactElement => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -44,20 +35,22 @@ export const PullList = (): ReactElement => {
}); });
const { const {
data: pullList, data: pullListData,
refetch, refetch,
isSuccess, isSuccess,
isLoading, isLoading,
isError, isError,
} = useQuery({ } = useGetWeeklyPullListQuery({
queryFn: async (): any => input: {
await cachedAxios(`${COMICVINE_SERVICE_URI}/getWeeklyPullList`, { startDate: inputValue,
method: "get", pageSize: 15,
params: { startDate: inputValue, pageSize: "15", currentPage: "1" }, currentPage: 1,
}), },
queryKey: ["pullList", inputValue],
}); });
// Transform the data to match the old structure
const pullList = pullListData ? { data: pullListData.getWeeklyPullList } : undefined;
const { mutate: addToLibrary } = useMutation({ const { mutate: addToLibrary } = useMutation({
mutationFn: async ({ sourceName, metadata }: { sourceName: string; metadata: any }) => { mutationFn: async ({ sourceName, metadata }: { sourceName: string; metadata: any }) => {
const comicBookMetadata = { const comicBookMetadata = {
@@ -146,7 +139,7 @@ export const PullList = (): ReactElement => {
{isSuccess && !isLoading && ( {isSuccess && !isLoading && (
<div className="overflow-hidden" ref={emblaRef}> <div className="overflow-hidden" ref={emblaRef}>
<div className="flex"> <div className="flex">
{map(pullList?.data.result, (issue, idx) => { {map(pullList?.data.result, (issue: LocgMetadata, idx: number) => {
return ( return (
<div <div
key={idx} key={idx}
@@ -154,13 +147,13 @@ export const PullList = (): ReactElement => {
> >
<Card <Card
orientation={"vertical-2"} orientation={"vertical-2"}
imageUrl={issue.coverImageUrl} imageUrl={issue.cover || undefined}
hasDetails hasDetails
title={ellipsize(issue.issueName, 25)} title={ellipsize(issue.name || 'Unknown', 25)}
> >
<div className="px-1"> <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"> <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> </span>
<div className="flex flex-row justify-end"> <div className="flex flex-row justify-end">
<button <button

View File

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

View File

@@ -5,12 +5,17 @@ import { Link, useNavigate } from "react-router-dom";
import Card from "../shared/Carda"; import Card from "../shared/Carda";
import Header from "../shared/Header"; import Header from "../shared/Header";
import useEmblaCarousel from "embla-carousel-react"; 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 // Till mongo gives us back the deduplicated results with the ObjectId
const deduplicatedGroups = unionBy(props.volumeGroups, "volumes.id"); const deduplicatedGroups = unionBy(props.volumeGroups, "volumes.id");
const navigate = useNavigate(); const navigate = useNavigate();
const navigateToVolumes = (row) => { const navigateToVolumes = (row: any) => {
navigate(`/volumes/all`); navigate(`/volumes/all`);
}; };
@@ -36,18 +41,18 @@ export const VolumeGroups = (props): ReactElement => {
{map(deduplicatedGroups, (data) => { {map(deduplicatedGroups, (data) => {
return ( return (
<div <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]" 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 <Card
orientation="vertical-2" orientation="vertical-2"
imageUrl={data.volumes.image.small_url} imageUrl={data.volumes?.image?.small_url || undefined}
hasDetails hasDetails
> >
<div className="py-3"> <div className="py-3">
<div className="text-sm"> <div className="text-sm">
<Link to={`/volume/details/${data._id}`}> <Link to={`/volume/details/${data.id}`}>
{ellipsize(data.volumes.name, 48)} {ellipsize(data.volumes?.name || 'Unknown', 48)}
</Link> </Link>
</div> </div>
{/* issue count */} {/* issue count */}
@@ -57,7 +62,7 @@ export const VolumeGroups = (props): ReactElement => {
</span> </span>
<span className="text-md text-slate-500 dark:text-slate-900"> <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>
</span> </span>
</div> </div>

View File

@@ -7,9 +7,10 @@ import { detectIssueTypes } from "../../shared/utils/tradepaperback.utils";
import { determineCoverFile } from "../../shared/utils/metadata.utils"; import { determineCoverFile } from "../../shared/utils/metadata.utils";
import Header from "../shared/Header"; import Header from "../shared/Header";
import useEmblaCarousel from "embla-carousel-react"; import useEmblaCarousel from "embla-carousel-react";
import { GetWantedComicsQuery } from "../../graphql/generated";
type WantedComicsListProps = { type WantedComicsListProps = {
comics: any; comics?: GetWantedComicsQuery['getComicBooks']['docs'];
}; };
export const WantedComicsList = ({ export const WantedComicsList = ({
@@ -38,12 +39,22 @@ export const WantedComicsList = ({
<div className="flex"> <div className="flex">
{map( {map(
comics, comics,
({ (comic) => {
_id, const {
rawFileDetails, id,
sourcedMetadata: { comicvine, comicInfo, locg }, rawFileDetails,
wanted, 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 isComicBookMetadataAvailable = !isUndefined(comicvine);
const consolidatedComicMetadata = { const consolidatedComicMetadata = {
rawFileDetails, rawFileDetails,
@@ -58,14 +69,14 @@ export const WantedComicsList = ({
publisher = null, publisher = null,
} = determineCoverFile(consolidatedComicMetadata); } = determineCoverFile(consolidatedComicMetadata);
const titleElement = ( const titleElement = (
<Link to={"/comic/details/" + _id}> <Link to={"/comic/details/" + id}>
{ellipsize(issueName, 20)} {ellipsize(issueName, 20)}
<p>{publisher}</p> <p>{publisher}</p>
</Link> </Link>
); );
return ( return (
<div <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]" 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 <Card
@@ -79,7 +90,7 @@ export const WantedComicsList = ({
<div className="flex flex-row gap-2"> <div className="flex flex-row gap-2">
{/* Issue type */} {/* Issue type */}
{isComicBookMetadataAvailable && {isComicBookMetadataAvailable &&
!isNil(detectIssueTypes(comicvine.description)) ? ( !isNil(detectIssueTypes(comicvine?.description)) ? (
<div className="my-2"> <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="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"> <span className="pr-1 pt-1">
@@ -88,29 +99,14 @@ export const WantedComicsList = ({
<span className="text-md text-slate-500 dark:text-slate-900"> <span className="text-md text-slate-500 dark:text-slate-900">
{ {
detectIssueTypes(comicvine.description) detectIssueTypes(comicvine?.description)
.displayName ?.displayName
} }
</span> </span>
</span> </span>
</div> </div>
) : null} ) : null}
{/* issues marked as wanted, part of this volume */} {/* Wanted comics - info not available in current GraphQL query */}
{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>
)}
</div> </div>
{/* comicVine metadata presence */} {/* comicVine metadata presence */}
{isComicBookMetadataAvailable && ( {isComicBookMetadataAvailable && (

View File

@@ -63,7 +63,6 @@ export const Downloads = (props: IDownloadsProps): ReactElement => {
<div className="columns"> <div className="columns">
<div className="column is-half"> <div className="column is-half">
{bundles.map((bundle, idx) => { {bundles.map((bundle, idx) => {
console.log(bundle);
return ( return (
<div key={idx}> <div key={idx}>
<MetadataPanel <MetadataPanel

View File

@@ -78,7 +78,6 @@ export const Search = ({}: ISearchProps): ReactElement => {
coverDate: cover_date, coverDate: cover_date,
issueNumber: issue_number, issueNumber: issue_number,
}); });
console.log(issues);
// Get volume metadata from CV // Get volume metadata from CV
const response = await axios({ const response = await axios({
url: `${COMICVINE_SERVICE_URI}/getVolumes`, url: `${COMICVINE_SERVICE_URI}/getVolumes`,
@@ -111,7 +110,6 @@ export const Search = ({}: ISearchProps): ReactElement => {
break; break;
default: default:
console.log("Invalid resource type.");
break; break;
} }
// Add to wanted list // Add to wanted list

View File

@@ -35,7 +35,6 @@ export const AirDCPPSettingsForm = () => {
// Handle setting update and subsequent AirDC++ initialization // Handle setting update and subsequent AirDC++ initialization
const { mutate } = useMutation({ const { mutate } = useMutation({
mutationFn: (values) => { mutationFn: (values) => {
console.log(values);
return axios.post("http://localhost:3000/api/settings/saveSettings", { return axios.post("http://localhost:3000/api/settings/saveSettings", {
settingsPayload: values, settingsPayload: values,
settingsKey: "directConnect", settingsKey: "directConnect",

View File

@@ -19,7 +19,6 @@ export const DockerVars = (): React.ReactElement => {
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
refetchOnReconnect: false, refetchOnReconnect: false,
}); });
console.log("Docker Vars: ", environmentVariables);
return ( return (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<h2 className="text-xl font-semibold">Docker Environment Variables</h2> <h2 className="text-xl font-semibold">Docker Environment Variables</h2>

View File

@@ -19,7 +19,6 @@ export const ProwlarrSettingsForm = (props) => {
}, },
queryKey: ["prowlarrConnectionResult"], queryKey: ["prowlarrConnectionResult"],
}); });
console.log(data);
const submitHandler = () => {}; const submitHandler = () => {};
const initialData = {}; const initialData = {};
return ( return (

View File

@@ -33,7 +33,6 @@ export const Volumes = (props): ReactElement => {
}), }),
queryKey: ["volumes"], queryKey: ["volumes"],
}); });
console.log(volumes);
const columnData = useMemo( const columnData = useMemo(
(): any => [ (): any => [
{ {
@@ -46,7 +45,6 @@ export const Volumes = (props): ReactElement => {
const { const {
_source: { sourcedMetadata }, _source: { sourcedMetadata },
} = comicObject; } = comicObject;
console.log("jaggu", row.getValue());
return ( return (
<div className="flex flex-row gap-3 mt-5"> <div className="flex flex-row gap-3 mt-5">
<Link to={`/volume/details/${comicObject._id}`}> <Link to={`/volume/details/${comicObject._id}`}>

View File

@@ -42,7 +42,6 @@ export const WantedComics = (props): ReactElement => {
minWidth: 350, minWidth: 350,
accessorFn: (data) => data, accessorFn: (data) => data,
cell: (value) => { cell: (value) => {
console.log("ASDASd", value);
const row = value.getValue()._source; const row = value.getValue()._source;
return row && <MetadataPanel data={row} />; return row && <MetadataPanel data={row} />;
}, },

View File

@@ -3,7 +3,7 @@ import { Link } from "react-router-dom";
type IHeaderProps = { type IHeaderProps = {
headerContent: string; headerContent: string;
subHeaderContent: ReactElement; subHeaderContent: ReactElement | string;
iconClassNames: string; iconClassNames: string;
link?: string; link?: string;
}; };

View File

@@ -0,0 +1,76 @@
import { GetComicByIdQuery } from '../generated';
/**
* Adapter to transform GraphQL Comic response to legacy REST API format
* This allows gradual migration while maintaining compatibility with existing components
*/
export function adaptGraphQLComicToLegacy(graphqlComic: GetComicByIdQuery['comic']) {
if (!graphqlComic) return null;
// Parse sourced metadata (GraphQL returns as strings)
const comicvine = graphqlComic.sourcedMetadata?.comicvine
? (typeof graphqlComic.sourcedMetadata.comicvine === 'string'
? JSON.parse(graphqlComic.sourcedMetadata.comicvine)
: graphqlComic.sourcedMetadata.comicvine)
: undefined;
const comicInfo = graphqlComic.sourcedMetadata?.comicInfo
? (typeof graphqlComic.sourcedMetadata.comicInfo === 'string'
? JSON.parse(graphqlComic.sourcedMetadata.comicInfo)
: graphqlComic.sourcedMetadata.comicInfo)
: undefined;
const locg = graphqlComic.sourcedMetadata?.locg || undefined;
// Use inferredMetadata from GraphQL response, or build from canonical metadata as fallback
const inferredMetadata = graphqlComic.inferredMetadata || {
issue: {
name: graphqlComic.canonicalMetadata?.title?.value ||
graphqlComic.canonicalMetadata?.series?.value ||
graphqlComic.rawFileDetails?.name || '',
number: graphqlComic.canonicalMetadata?.issueNumber?.value
? parseInt(graphqlComic.canonicalMetadata.issueNumber.value, 10)
: undefined,
year: graphqlComic.canonicalMetadata?.publicationDate?.value?.substring(0, 4) ||
graphqlComic.canonicalMetadata?.coverDate?.value?.substring(0, 4),
subtitle: graphqlComic.canonicalMetadata?.series?.value,
},
};
// Build acquisition data (if available from importStatus or other fields)
const acquisition = {
directconnect: {
downloads: [],
},
torrent: [],
};
// Transform rawFileDetails to match expected format
const rawFileDetails = graphqlComic.rawFileDetails ? {
name: graphqlComic.rawFileDetails.name || '',
filePath: graphqlComic.rawFileDetails.filePath,
fileSize: graphqlComic.rawFileDetails.fileSize,
extension: graphqlComic.rawFileDetails.extension,
mimeType: graphqlComic.rawFileDetails.mimeType,
containedIn: graphqlComic.rawFileDetails.containedIn,
pageCount: graphqlComic.rawFileDetails.pageCount,
archive: graphqlComic.rawFileDetails.archive,
cover: graphqlComic.rawFileDetails.cover,
} : undefined;
return {
_id: graphqlComic.id,
rawFileDetails,
inferredMetadata,
sourcedMetadata: {
comicvine,
locg,
comicInfo,
},
acquisition,
createdAt: graphqlComic.createdAt || new Date().toISOString(),
updatedAt: graphqlComic.updatedAt || new Date().toISOString(),
// Include the full GraphQL data for components that can use it
__graphql: graphqlComic,
} as any; // Use 'as any' to bypass strict type checking during migration
}

View File

@@ -0,0 +1,19 @@
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
import { LIBRARY_SERVICE_HOST } from '../constants/endpoints';
const httpLink = new HttpLink({
uri: `${LIBRARY_SERVICE_HOST}/graphql`,
});
export const apolloClient = new ApolloClient({
link: httpLink,
cache: new InMemoryCache(),
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network',
},
query: {
fetchPolicy: 'network-only',
},
},
});

View File

@@ -0,0 +1,43 @@
import { LIBRARY_SERVICE_HOST } from '../constants/endpoints';
export function fetcher<TData, TVariables>(
query: string,
variables?: TVariables,
options?: RequestInit['headers']
) {
return async (): Promise<TData> => {
try {
const res = await fetch(`${LIBRARY_SERVICE_HOST}/graphql`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...options,
},
body: JSON.stringify({
query,
variables,
}),
});
// Check if the response is OK (status 200-299)
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
const json = await res.json();
if (json.errors) {
const { message } = json.errors[0] || {};
throw new Error(message || 'Error fetching data');
}
return json.data;
} catch (error) {
// Handle network errors or other fetch failures
if (error instanceof Error) {
throw new Error(`Failed to fetch: ${error.message}`);
}
throw new Error('Failed to fetch data from server');
}
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,191 @@
query GetComicById($id: ID!) {
comic(id: $id) {
id
# Inferred metadata
inferredMetadata {
issue {
name
number
year
subtitle
}
}
# Canonical metadata
canonicalMetadata {
title {
value
provenance {
source
sourceId
confidence
fetchedAt
url
}
userOverride
}
series {
value
provenance {
source
sourceId
confidence
fetchedAt
url
}
userOverride
}
volume {
value
provenance {
source
sourceId
confidence
fetchedAt
url
}
userOverride
}
issueNumber {
value
provenance {
source
sourceId
confidence
fetchedAt
url
}
userOverride
}
publisher {
value
provenance {
source
sourceId
confidence
fetchedAt
url
}
userOverride
}
publicationDate {
value
provenance {
source
sourceId
confidence
fetchedAt
url
}
userOverride
}
coverDate {
value
provenance {
source
sourceId
confidence
fetchedAt
url
}
userOverride
}
description {
value
provenance {
source
sourceId
confidence
fetchedAt
url
}
userOverride
}
creators {
name
role
provenance {
source
sourceId
confidence
fetchedAt
url
}
}
pageCount {
value
provenance {
source
sourceId
confidence
fetchedAt
url
}
userOverride
}
coverImage {
value
provenance {
source
sourceId
confidence
fetchedAt
url
}
userOverride
}
}
# Sourced metadata
sourcedMetadata {
comicInfo
comicvine
metron
gcd
locg {
name
publisher
url
cover
description
price
rating
pulls
potw
}
}
# Raw file details
rawFileDetails {
name
filePath
fileSize
extension
mimeType
containedIn
pageCount
archive {
uncompressed
expandedPath
}
cover {
filePath
stats
}
}
# Import status
importStatus {
isImported
tagged
matchedResult {
score
}
}
# Timestamps
createdAt
updatedAt
}
}

View File

@@ -0,0 +1,216 @@
query GetComics($page: Int, $limit: Int, $search: String, $publisher: String, $series: String) {
comics(page: $page, limit: $limit, search: $search, publisher: $publisher, series: $series) {
comics {
id
inferredMetadata {
issue {
name
number
year
subtitle
}
}
rawFileDetails {
name
extension
archive {
uncompressed
}
}
sourcedMetadata {
comicvine
comicInfo
locg {
name
publisher
cover
}
}
canonicalMetadata {
title {
value
}
series {
value
}
issueNumber {
value
}
}
}
totalCount
pageInfo {
hasNextPage
hasPreviousPage
currentPage
totalPages
}
}
}
query GetRecentComics($limit: Int) {
comics(page: 1, limit: $limit) {
comics {
id
inferredMetadata {
issue {
name
number
year
subtitle
}
}
rawFileDetails {
name
extension
cover {
filePath
}
archive {
uncompressed
}
}
sourcedMetadata {
comicvine
comicInfo
locg {
name
publisher
cover
}
}
canonicalMetadata {
title {
value
}
series {
value
}
issueNumber {
value
}
publisher {
value
}
}
createdAt
updatedAt
}
totalCount
}
}
query GetWantedComics($paginationOptions: PaginationOptionsInput!, $predicate: PredicateInput) {
getComicBooks(paginationOptions: $paginationOptions, predicate: $predicate) {
docs {
id
inferredMetadata {
issue {
name
number
year
subtitle
}
}
rawFileDetails {
name
extension
cover {
filePath
}
archive {
uncompressed
}
}
sourcedMetadata {
comicvine
comicInfo
locg {
name
publisher
cover
}
}
canonicalMetadata {
title {
value
}
series {
value
}
issueNumber {
value
}
}
createdAt
updatedAt
}
totalDocs
limit
page
totalPages
hasNextPage
hasPrevPage
}
}
query GetVolumeGroups {
getComicBookGroups {
id
volumes {
id
name
count_of_issues
publisher {
id
name
}
start_year
image {
small_url
}
}
}
}
query GetLibraryStatistics {
getLibraryStatistics {
totalDocuments
comicDirectorySize {
fileCount
}
statistics {
fileTypes {
id
data
}
issues {
id {
id
name
}
data
}
fileLessComics {
id
}
issuesWithComicInfoXML {
id
}
publisherWithMostComicsInLibrary {
id
count
}
}
}
}
query GetWeeklyPullList($input: WeeklyPullListInput!) {
getWeeklyPullList(input: $input) {
result {
name
publisher
cover
}
}
}

View File

@@ -0,0 +1,72 @@
# Library queries
# Note: The Library component currently uses Elasticsearch for search functionality
# These queries are prepared for when the backend supports GraphQL-based library queries
query GetLibraryComics($page: Int, $limit: Int, $search: String, $series: String) {
comics(page: $page, limit: $limit, search: $search, series: $series) {
comics {
id
inferredMetadata {
issue {
name
number
year
subtitle
}
}
rawFileDetails {
name
filePath
fileSize
extension
mimeType
pageCount
archive {
uncompressed
}
cover {
filePath
}
}
sourcedMetadata {
comicvine
comicInfo
locg {
name
publisher
cover
}
}
canonicalMetadata {
title {
value
}
series {
value
}
issueNumber {
value
}
publisher {
value
}
pageCount {
value
}
}
importStatus {
isImported
tagged
}
createdAt
updatedAt
}
totalCount
pageInfo {
hasNextPage
hasPreviousPage
currentPage
totalPages
}
}
}

View File

@@ -18,7 +18,14 @@ import Volumes from "./components/Volumes/Volumes";
import VolumeDetails from "./components/VolumeDetail/VolumeDetail"; import VolumeDetails from "./components/VolumeDetail/VolumeDetail";
import WantedComics from "./components/WantedComics/WantedComics"; import WantedComics from "./components/WantedComics/WantedComics";
const queryClient = new QueryClient(); const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
refetchOnWindowFocus: false,
},
},
});
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {

View File

@@ -47,7 +47,6 @@ const onSearchSent = async (item, instance, unsubscribe, searchInfo) => {
if (results.length > 0) { if (results.length > 0) {
// We have results, download the best one // We have results, download the best one
console.log("SASAAAA", results);
// const result = results[0]; // const result = results[0];
// SocketService.post(`search/${instance.id}/results/${result.id}/download`, { // SocketService.post(`search/${instance.id}/results/${result.id}/download`, {
// priority: Utils.toApiPriority(item.priority), // priority: Utils.toApiPriority(item.priority),

View File

@@ -42,6 +42,6 @@ const getIssueTypeDisplayName = (
return { displayName, results }; return { displayName, results };
} }
} catch (error) { } catch (error) {
console.log(error); // Error handling could be added here if needed
} }
}; };

View File

@@ -47,12 +47,11 @@ export const useStore = create<StoreState>((set, get) => ({
}); });
socket.on("connect", () => { socket.on("connect", () => {
console.log(`✅ Connected to ${namespace}:`, socket.id); // Socket connected successfully
}); });
// Always listen for sessionInitialized in case backend creates a new session // Always listen for sessionInitialized in case backend creates a new session
socket.on("sessionInitialized", (id) => { socket.on("sessionInitialized", (id) => {
console.log("Session initialized with ID:", id);
localStorage.setItem("sessionId", id); localStorage.setItem("sessionId", id);
}); });

View File

@@ -6,3 +6,6 @@ declare module "*.png" {
declare module "*.jpg"; declare module "*.jpg";
declare module "*.gif"; declare module "*.gif";
declare module "*.less"; declare module "*.less";
// Comic types are now generated from GraphQL schema
// Import from '../../graphql/generated' instead

View File

@@ -21,21 +21,7 @@ app.use(function (req, res, next) {
// Getting data from body of requests // Getting data from body of requests
app.use(bodyParser.json()); app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.urlencoded({ extended: true }));
//
// app.get("/", (req: Request, res: Response) => {
// console.log("sending index.html");
// res.sendFile(path.resolve(__dirname, "../dist/index.html"));
// });
// REGISTER ROUTES // REGISTER ROUTES
// all of the routes will be prefixed with /api // all of the routes will be prefixed with /api
const routes: Router[] = Object.values(router); const routes: Router[] = Object.values(router);
// app.use("/api", routes);
// Send index.html on root request
// app.use(express.static("dist"));
// app.use(express.static("public"));
// app.listen(port);
// console.log(`Server is listening on ${port}`);

3217
yarn.lock

File diff suppressed because it is too large Load Diff