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 } })), }, }));