diff --git a/src/client/components/Import/Import.tsx b/src/client/components/Import/Import.tsx
index 4b13fcd..c522bc3 100644
--- a/src/client/components/Import/Import.tsx
+++ b/src/client/components/Import/Import.tsx
@@ -307,7 +307,7 @@ export const Import = (props: IProps): ReactElement => {
{format(
new Date(jobResult.earliestTimestamp),
- "EEEE, hh:mma, do LLLL Y",
+ "EEEE, hh:mma, do LLLL y",
)}
|
diff --git a/src/client/graphql/generated.ts b/src/client/graphql/generated.ts
index 0998a8a..6f7cd59 100644
--- a/src/client/graphql/generated.ts
+++ b/src/client/graphql/generated.ts
@@ -278,23 +278,9 @@ export type InferredMetadataInput = {
export type Issue = {
__typename?: 'Issue';
- api_detail_url?: Maybe;
- character_credits?: Maybe>;
- cover_date?: Maybe;
- description?: Maybe;
- id: Scalars['Int']['output'];
- image?: Maybe;
- issue_number?: Maybe;
- location_credits?: Maybe>;
name?: Maybe;
number?: Maybe;
- person_credits?: Maybe>;
- site_detail_url?: Maybe;
- store_date?: Maybe;
- story_arc_credits?: Maybe>;
subtitle?: Maybe;
- team_credits?: Maybe>;
- volume?: Maybe;
year?: Maybe;
};
@@ -534,9 +520,10 @@ export type PullListItem = {
__typename?: 'PullListItem';
cover?: Maybe;
description?: Maybe;
- name: Scalars['String']['output'];
+ name?: Maybe;
potw?: Maybe;
price?: Maybe;
+ publicationDate?: Maybe;
publisher?: Maybe;
pulls?: Maybe;
rating?: Maybe;
@@ -1009,7 +996,7 @@ export type GetWeeklyPullListQueryVariables = Exact<{
}>;
-export type GetWeeklyPullListQuery = { __typename?: 'Query', getWeeklyPullList: { __typename?: 'PullListResponse', result: Array<{ __typename?: 'PullListItem', name: string, publisher?: string | null, cover?: string | null }> } };
+export type GetWeeklyPullListQuery = { __typename?: 'Query', getWeeklyPullList: { __typename?: 'PullListResponse', result: Array<{ __typename?: 'PullListItem', name?: string | null, publisher?: string | null, cover?: string | null }> } };
export type GetLibraryComicsQueryVariables = Exact<{
page?: InputMaybe;
diff --git a/src/client/store/index.ts b/src/client/store/index.ts
index 0625dd0..7e70b6b 100644
--- a/src/client/store/index.ts
+++ b/src/client/store/index.ts
@@ -1,177 +1,120 @@
import { create } from "zustand";
import io, { Socket } from "socket.io-client";
import { SOCKET_BASE_URI } from "../constants/endpoints";
-import { isNil } from "lodash";
import { QueryClient } from "@tanstack/react-query";
import { toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
const queryClient = new QueryClient();
-// Type for global state
+/**
+ * Global application state interface
+ */
interface StoreState {
+ /** Active socket.io connections by namespace */
socketInstances: Record;
+ /**
+ * Get or create socket connection for namespace
+ * @param namespace - Socket namespace (default: "/")
+ * @returns Socket instance
+ */
getSocket: (namespace?: string) => Socket;
+ /**
+ * Disconnect and remove socket instance
+ * @param namespace - Socket namespace to disconnect
+ */
disconnectSocket: (namespace: string) => void;
-
+ /** ComicVine scraping state */
comicvine: {
scrapingStatus: string;
};
-
+ /** Import job queue state and actions */
importJobQueue: {
successfulJobCount: number;
failedJobCount: number;
status: string | undefined;
mostRecentImport: string | null;
-
setStatus: (status: string) => void;
setJobCount: (jobType: string, count: number) => void;
setMostRecentImport: (fileName: string) => void;
};
}
+/**
+ * Zustand store for global app state and socket management
+ */
export const useStore = create((set, get) => ({
socketInstances: {},
getSocket: (namespace = "/") => {
- const fullNamespace = namespace === "/" ? "" : namespace;
+ const ns = namespace === "/" ? "" : namespace;
const existing = get().socketInstances[namespace];
-
- if (existing && existing.connected) return existing;
+ if (existing?.connected) return existing;
const sessionId = localStorage.getItem("sessionId");
- const socket = io(`${SOCKET_BASE_URI}${fullNamespace}`, {
+ const socket = io(`${SOCKET_BASE_URI}${ns}`, {
transports: ["websocket"],
withCredentials: true,
query: { sessionId },
});
- socket.on("connect", () => {
- // Socket connected successfully
- });
+ socket.on("sessionInitialized", (id) => localStorage.setItem("sessionId", id));
+ if (sessionId) socket.emit("call", "socket.resumeSession", { sessionId, namespace });
- // Always listen for sessionInitialized in case backend creates a new session
- socket.on("sessionInitialized", (id) => {
- localStorage.setItem("sessionId", id);
- });
+ socket.on("RESTORE_JOB_COUNTS_AFTER_SESSION_RESTORATION", ({ completedJobCount, failedJobCount, queueStatus }) =>
+ set((s) => ({ importJobQueue: { ...s.importJobQueue, successfulJobCount: completedJobCount, failedJobCount, status: queueStatus } }))
+ );
- if (sessionId) {
- socket.emit("call", "socket.resumeSession", { sessionId, namespace });
- }
+ socket.on("LS_COVER_EXTRACTED", ({ completedJobCount, importResult }) =>
+ set((s) => ({ importJobQueue: { ...s.importJobQueue, successfulJobCount: completedJobCount, mostRecentImport: importResult.data.rawFileDetails.name } }))
+ );
- socket.on("RESTORE_JOB_COUNTS_AFTER_SESSION_RESTORATION", (data) => {
- const { completedJobCount, failedJobCount, queueStatus } = data;
- set((state) => ({
- importJobQueue: {
- ...state.importJobQueue,
- successfulJobCount: completedJobCount,
- failedJobCount,
- status: queueStatus,
- },
- }));
- });
-
- socket.on("LS_COVER_EXTRACTED", ({ completedJobCount, importResult }) => {
- set((state) => ({
- importJobQueue: {
- ...state.importJobQueue,
- successfulJobCount: completedJobCount,
- mostRecentImport: importResult.data.rawFileDetails.name,
- },
- }));
- });
-
- socket.on("LS_COVER_EXTRACTION_FAILED", ({ failedJobCount }) => {
- set((state) => ({
- importJobQueue: {
- ...state.importJobQueue,
- failedJobCount,
- },
- }));
- });
+ socket.on("LS_COVER_EXTRACTION_FAILED", ({ failedJobCount }) =>
+ set((s) => ({ importJobQueue: { ...s.importJobQueue, failedJobCount } }))
+ );
socket.on("LS_IMPORT_QUEUE_DRAINED", () => {
- set((state) => ({
- importJobQueue: {
- ...state.importJobQueue,
- status: "drained",
- },
- }));
- // Delay query invalidation and sessionId removal to ensure backend has persisted data
+ set((s) => ({ importJobQueue: { ...s.importJobQueue, status: "drained" } }));
setTimeout(() => {
queryClient.invalidateQueries({ queryKey: ["allImportJobResults"] });
localStorage.removeItem("sessionId");
}, 500);
});
- socket.on("CV_SCRAPING_STATUS", (data) => {
- set((state) => ({
- comicvine: {
- ...state.comicvine,
- scrapingStatus: data.message,
- },
- }));
- });
+ socket.on("CV_SCRAPING_STATUS", ({ message }) =>
+ set((s) => ({ comicvine: { ...s.comicvine, scrapingStatus: message } }))
+ );
- socket.on("searchResultsAvailable", (data) => {
- toast(`Results found for query: ${JSON.stringify(data.query, null, 2)}`);
- });
-
- set((state) => ({
- socketInstances: {
- ...state.socketInstances,
- [namespace]: socket,
- },
- }));
+ socket.on("searchResultsAvailable", ({ query }) =>
+ toast(`Results found for query: ${JSON.stringify(query, null, 2)}`)
+ );
+ set((s) => ({ socketInstances: { ...s.socketInstances, [namespace]: socket } }));
return socket;
},
- disconnectSocket: (namespace: string) => {
+ disconnectSocket: (namespace) => {
const socket = get().socketInstances[namespace];
if (socket) {
socket.disconnect();
- set((state) => {
- const { [namespace]: _, ...rest } = state.socketInstances;
+ set((s) => {
+ const { [namespace]: _, ...rest } = s.socketInstances;
return { socketInstances: rest };
});
}
},
- comicvine: {
- scrapingStatus: "",
- },
+ comicvine: { scrapingStatus: "" },
importJobQueue: {
successfulJobCount: 0,
failedJobCount: 0,
status: undefined,
mostRecentImport: null,
-
- setStatus: (status: string) =>
- set((state) => ({
- importJobQueue: {
- ...state.importJobQueue,
- status,
- },
- })),
-
- setJobCount: (jobType: string, count: number) =>
- set((state) => ({
- importJobQueue: {
- ...state.importJobQueue,
- ...(jobType === "successful"
- ? { successfulJobCount: count }
- : { failedJobCount: count }),
- },
- })),
-
- setMostRecentImport: (fileName: string) =>
- set((state) => ({
- importJobQueue: {
- ...state.importJobQueue,
- mostRecentImport: fileName,
- },
- })),
+ setStatus: (status) => set((s) => ({ importJobQueue: { ...s.importJobQueue, status } })),
+ setJobCount: (jobType, count) => set((s) => ({
+ importJobQueue: { ...s.importJobQueue, [jobType === "successful" ? "successfulJobCount" : "failedJobCount"]: count }
+ })),
+ setMostRecentImport: (fileName) => set((s) => ({ importJobQueue: { ...s.importJobQueue, mostRecentImport: fileName } })),
},
}));
|