From 2dc38b6c95ad87275ab7f88f53c53e7c31a7427a Mon Sep 17 00:00:00 2001 From: Rishi Ghan Date: Wed, 15 Apr 2026 11:31:52 -0400 Subject: [PATCH] JsDoc across all project files --- src/client/actions/airdcpp.actions.tsx | 71 +++++++++++++++ src/client/actions/comicinfo.actions.tsx | 86 ++++++++++++++++++ src/client/actions/fileops.actions.tsx | 14 +++ src/client/actions/metron.actions.tsx | 29 ++++++ src/client/actions/settings.actions.tsx | 48 ++++++++++ src/client/components/App.tsx | 27 ++++++ src/client/components/Import/Import.tsx | 31 +++++++ .../components/Import/RealTimeImportStats.tsx | 9 +- src/client/components/shared/AlertCard.tsx | 11 +++ src/client/components/shared/ProgressBar.tsx | 10 +++ src/client/components/shared/StatsCard.tsx | 10 +++ src/client/constants/action-types.ts | 15 ++++ src/client/constants/endpoints.ts | 28 ++++++ src/client/graphql/adapters/comicAdapter.ts | 37 +++++++- src/client/graphql/fetcher.ts | 27 ++++++ src/client/hooks/useDarkMode.tsx | 28 ++++++ src/client/hooks/useImportSessionStatus.ts | 57 +++++++++--- src/client/hooks/useImportSocketEvents.ts | 41 ++++++++- src/client/services/DcppSearchService.ts | 32 +++++++ src/client/services/api/SearchApi.ts | 48 ++++++++++ src/client/shared/utils/formatting.utils.ts | 55 +++++++++++- src/client/shared/utils/i18n.util.ts | 42 +++++++-- src/client/shared/utils/metadata.utils.ts | 90 ++++++++++++++++--- src/client/shared/utils/object.utils.ts | 73 +++++++++++++-- .../shared/utils/tradepaperback.utils.ts | 47 +++++++++- src/client/shared/utils/validator.utils.ts | 21 ++++- src/client/store/index.ts | 16 +++- src/client/types/index.d.ts | 46 +++++++++- 28 files changed, 999 insertions(+), 50 deletions(-) diff --git a/src/client/actions/airdcpp.actions.tsx b/src/client/actions/airdcpp.actions.tsx index 1fd962e..ba50303 100644 --- a/src/client/actions/airdcpp.actions.tsx +++ b/src/client/actions/airdcpp.actions.tsx @@ -1,3 +1,10 @@ +/** + * @fileoverview Redux action creators for AirDC++ integration. + * Provides actions for Direct Connect protocol operations including + * search, download management, and socket connection handling. + * @module actions/airdcpp + */ + import { SearchQuery, SearchInstance, @@ -27,16 +34,41 @@ import { import { isNil } from "lodash"; import axios from "axios"; +/** + * Configuration data for an AirDC++ search operation. + * @interface SearchData + * @property {Object} query - Search query parameters + * @property {string} query.pattern - Search pattern/term (required) + * @property {string[]|undefined|null} hub_urls - List of hub URLs to search + * @property {PriorityEnum} priority - Download priority level + */ interface SearchData { query: Pick & Partial>; hub_urls: string[] | undefined | null; priority: PriorityEnum; } +/** + * Creates a promise that resolves after a specified delay. + * Useful for rate limiting or adding delays between operations. + * + * @param {number} ms - Number of milliseconds to sleep + * @returns {Promise} Promise that resolves after the delay + * @example + * await sleep(1000); // Wait 1 second + */ export const sleep = (ms: number): Promise => { return new Promise((resolve) => setTimeout(resolve, ms)); }; +/** + * Redux thunk action creator to toggle AirDC++ socket connection status. + * Dispatches connection or disconnection events to update application state. + * + * @param {String} status - Connection status ("connected" or "disconnected") + * @param {any} [payload] - Optional payload data for the status change + * @returns {Function} Redux thunk function + */ export const toggleAirDCPPSocketConnectionStatus = (status: String, payload?: any) => async (dispatch) => { switch (status) { @@ -58,6 +90,23 @@ export const toggleAirDCPPSocketConnectionStatus = break; } }; + +/** + * Redux thunk action creator to download an item from AirDC++ search results. + * Initiates the download, stores bundle metadata in the database, and updates + * the comic book record with download information. + * + * @param {Number} searchInstanceId - ID of the active search instance + * @param {String} resultId - ID of the search result to download + * @param {String} comicObjectId - ID of the comic book in the database + * @param {String} name - Name of the file to download + * @param {Number} size - Size of the file in bytes + * @param {any} type - File type information + * @param {any} ADCPPSocket - AirDC++ socket connection instance + * @param {any} credentials - Authentication credentials for AirDC++ + * @returns {Function} Redux thunk function + * @throws {Error} If download initiation or database update fails + */ export const downloadAirDCPPItem = ( searchInstanceId: Number, @@ -112,6 +161,17 @@ export const downloadAirDCPPItem = } }; +/** + * Redux thunk action creator to fetch AirDC++ download bundles for a specific comic. + * Retrieves the comic book record, extracts associated bundle IDs, and fetches + * bundle details from the AirDC++ queue. + * + * @param {string} comicObjectId - ID of the comic book in the database + * @param {any} ADCPPSocket - AirDC++ socket connection instance + * @param {any} credentials - Authentication credentials for AirDC++ + * @returns {Function} Redux thunk function that dispatches AIRDCPP_BUNDLES_FETCHED + * @throws {Error} If fetching comic or bundles fails + */ export const getBundlesForComic = (comicObjectId: string, ADCPPSocket: any, credentials: any) => async (dispatch) => { @@ -147,6 +207,17 @@ export const getBundlesForComic = } }; +/** + * Redux thunk action creator to fetch all active transfers from AirDC++. + * Retrieves current download bundles and groups library issues by their + * associated bundle IDs for display in the UI. + * + * @param {any} ADCPPSocket - AirDC++ socket connection instance + * @param {any} credentials - Authentication credentials for AirDC++ + * @returns {Function} Redux thunk function that dispatches AIRDCPP_TRANSFERS_FETCHED + * and LIBRARY_ISSUE_BUNDLES actions + * @throws {Error} If fetching transfers fails + */ export const getTransfers = (ADCPPSocket: any, credentials: any) => async (dispatch) => { try { diff --git a/src/client/actions/comicinfo.actions.tsx b/src/client/actions/comicinfo.actions.tsx index a824fc1..ec112b5 100644 --- a/src/client/actions/comicinfo.actions.tsx +++ b/src/client/actions/comicinfo.actions.tsx @@ -1,3 +1,10 @@ +/** + * @fileoverview Redux action creators for ComicVine API and comic book information. + * Provides actions for searching ComicVine, fetching comic metadata, managing + * library statistics, and applying ComicVine matches to local comic records. + * @module actions/comicinfo + */ + import axios from "axios"; import rateLimiter from "axios-rate-limit"; import { setupCache } from "axios-cache-interceptor"; @@ -22,12 +29,30 @@ import { LIBRARY_SERVICE_BASE_URI, } from "../constants/endpoints"; +/** + * Rate-limited axios instance for ComicVine API calls. + * Limited to 1 request per second to comply with API rate limits. + * @constant {AxiosInstance} + */ const http = rateLimiter(axios.create(), { maxRequests: 1, perMilliseconds: 1000, maxRPS: 1, }); + +/** + * Cached axios instance for reducing redundant API calls. + * @constant {AxiosInstance} + */ const cachedAxios = setupCache(axios); + +/** + * Redux thunk action creator to fetch the weekly comic pull list. + * Retrieves upcoming comic releases from the ComicVine service. + * + * @param {Object} options - Query parameters for the pull list request + * @returns {Function} Redux thunk function that dispatches CV_WEEKLY_PULLLIST_FETCHED + */ export const getWeeklyPullList = (options) => async (dispatch) => { try { dispatch({ @@ -47,6 +72,18 @@ export const getWeeklyPullList = (options) => async (dispatch) => { } }; +/** + * Generic Redux thunk action creator for ComicVine API calls. + * Handles rate-limited requests to the ComicVine service with configurable + * endpoints, methods, and parameters. + * + * @param {Object} options - API call configuration options + * @param {string} options.callURIAction - API endpoint action (e.g., "search") + * @param {string} options.callMethod - HTTP method (GET, POST, etc.) + * @param {Object} options.callParams - Query parameters for the request + * @param {any} [options.data] - Request body data + * @returns {Function} Redux thunk function that dispatches appropriate action based on callURIAction + */ export const comicinfoAPICall = (options) => async (dispatch) => { try { dispatch({ @@ -82,6 +119,14 @@ export const comicinfoAPICall = (options) => async (dispatch) => { }); } }; + +/** + * Redux thunk action creator to fetch all issues for a comic series. + * Retrieves issue list from ComicVine for a given volume/series. + * + * @param {string} comicObjectID - ComicVine volume/series ID + * @returns {Function} Redux thunk function that dispatches CV_ISSUES_FOR_VOLUME_IN_LIBRARY_SUCCESS + */ export const getIssuesForSeries = (comicObjectID: string) => async (dispatch) => { dispatch({ @@ -104,6 +149,18 @@ export const getIssuesForSeries = }); }; +/** + * Redux thunk action creator to analyze library issues against ComicVine data. + * Maps issues to query objects and finds matching issues in the local library. + * + * @param {Array} issues - Array of ComicVine issue objects to analyze + * @param {string} issues[].id - Issue ID + * @param {string} issues[].name - Issue name + * @param {string} issues[].issue_number - Issue number + * @param {Object} issues[].volume - Volume information + * @param {string} issues[].volume.name - Volume name + * @returns {Function} Redux thunk function that dispatches CV_ISSUES_MATCHES_IN_LIBRARY_FETCHED + */ export const analyzeLibrary = (issues) => async (dispatch) => { dispatch({ type: CV_ISSUES_METADATA_CALL_IN_PROGRESS, @@ -131,6 +188,12 @@ export const analyzeLibrary = (issues) => async (dispatch) => { }); }; +/** + * Redux thunk action creator to fetch library statistics. + * Retrieves aggregate statistics about the comic library. + * + * @returns {Function} Redux thunk function that dispatches LIBRARY_STATISTICS_FETCHED + */ export const getLibraryStatistics = () => async (dispatch) => { dispatch({ type: LIBRARY_STATISTICS_CALL_IN_PROGRESS, @@ -146,6 +209,13 @@ export const getLibraryStatistics = () => async (dispatch) => { }); }; +/** + * Redux thunk action creator to fetch detailed comic book information. + * Retrieves full comic book document from the library database by ID. + * + * @param {string} comicBookObjectId - Database ID of the comic book + * @returns {Function} Redux thunk function that dispatches IMS_COMIC_BOOK_DB_OBJECT_FETCHED + */ export const getComicBookDetailById = (comicBookObjectId: string) => async (dispatch) => { dispatch({ @@ -166,6 +236,13 @@ export const getComicBookDetailById = }); }; +/** + * Redux thunk action creator to fetch multiple comic books by their IDs. + * Retrieves full comic book documents from the library database for a list of IDs. + * + * @param {Array} comicBookObjectIds - Array of database IDs + * @returns {Function} Redux thunk function that dispatches IMS_COMIC_BOOKS_DB_OBJECTS_FETCHED + */ export const getComicBooksDetailsByIds = (comicBookObjectIds: Array) => async (dispatch) => { dispatch({ @@ -185,6 +262,15 @@ export const getComicBooksDetailsByIds = }); }; +/** + * Redux thunk action creator to apply ComicVine metadata to a local comic. + * Associates a ComicVine match with a comic book record in the database, + * updating the comic with metadata from ComicVine. + * + * @param {Object} match - ComicVine match object containing metadata to apply + * @param {string} comicObjectId - Database ID of the local comic book to update + * @returns {Function} Redux thunk function that dispatches IMS_COMIC_BOOK_DB_OBJECT_FETCHED + */ export const applyComicVineMatch = (match, comicObjectId) => async (dispatch) => { dispatch({ diff --git a/src/client/actions/fileops.actions.tsx b/src/client/actions/fileops.actions.tsx index f1162be..20e06cd 100644 --- a/src/client/actions/fileops.actions.tsx +++ b/src/client/actions/fileops.actions.tsx @@ -1,3 +1,10 @@ +/** + * @fileoverview Redux action creators for file operations and library management. + * Provides actions for importing comics, searching the library, managing folders, + * extracting comic archives, and analyzing images. + * @module actions/fileops + */ + import axios from "axios"; import { IFolderData } from "threetwo-ui-typings"; import { @@ -37,6 +44,13 @@ import { import { isNil } from "lodash"; +/** + * Redux thunk action creator to fetch library service health status. + * Retrieves health information from the library microservice. + * + * @param {string} [serviceName] - Optional specific service name to check + * @returns {Function} Redux thunk function that dispatches LIBRARY_SERVICE_HEALTH + */ export const getServiceStatus = (serviceName?: string) => async (dispatch) => { axios .request({ diff --git a/src/client/actions/metron.actions.tsx b/src/client/actions/metron.actions.tsx index 88cb4ec..ea58da6 100644 --- a/src/client/actions/metron.actions.tsx +++ b/src/client/actions/metron.actions.tsx @@ -1,7 +1,36 @@ +/** + * @fileoverview Action creators for Metron comic database API integration. + * Provides functions to fetch comic metadata resources from the Metron service. + * @module actions/metron + */ + import axios from "axios"; import { isNil } from "lodash"; import { METRON_SERVICE_URI } from "../constants/endpoints"; +/** + * @typedef {Object} MetronResourceResult + * @property {Array<{label: string, value: number}>} options - Formatted options for select components + * @property {boolean} hasMore - Whether more results are available + * @property {Object} additional - Additional pagination data + * @property {number|null} additional.page - Next page number, or null if no more pages + */ + +/** + * Fetches comic resources from the Metron API service. + * Returns formatted data suitable for async select/dropdown components. + * + * @async + * @param {Object} options - Request options for the Metron API + * @param {Object} options.query - Query parameters + * @param {number} options.query.page - Current page number for pagination + * @returns {Promise} Formatted results with pagination info + * @example + * const results = await fetchMetronResource({ + * query: { page: 1, resource: "publishers" } + * }); + * // Returns: { options: [{label: "DC Comics", value: 1}], hasMore: true, additional: { page: 2 } } + */ export const fetchMetronResource = async (options) => { const metronResourceResults = await axios.post( `${METRON_SERVICE_URI}/fetchResource`, diff --git a/src/client/actions/settings.actions.tsx b/src/client/actions/settings.actions.tsx index 384783a..4fce4bb 100644 --- a/src/client/actions/settings.actions.tsx +++ b/src/client/actions/settings.actions.tsx @@ -1,3 +1,10 @@ +/** + * @fileoverview Redux action creators for application settings management. + * Provides actions for fetching, deleting, and managing settings, as well as + * integrations with external services like qBittorrent and Prowlarr. + * @module actions/settings + */ + import axios from "axios"; import { SETTINGS_OBJECT_FETCHED, @@ -11,6 +18,13 @@ import { QBITTORRENT_SERVICE_BASE_URI, } from "../constants/endpoints"; +/** + * Redux thunk action creator to fetch application settings. + * Can retrieve all settings or a specific settings key. + * + * @param {string} [settingsKey] - Optional specific settings key to fetch + * @returns {Function} Redux thunk function that dispatches SETTINGS_OBJECT_FETCHED + */ export const getSettings = (settingsKey?) => async (dispatch) => { const result = await axios({ url: `${SETTINGS_SERVICE_BASE_URI}/getSettings`, @@ -25,6 +39,12 @@ export const getSettings = (settingsKey?) => async (dispatch) => { } }; +/** + * Redux thunk action creator to delete all application settings. + * Clears the settings from the database and resets state to empty object. + * + * @returns {Function} Redux thunk function that dispatches SETTINGS_OBJECT_FETCHED with empty data + */ export const deleteSettings = () => async (dispatch) => { const result = await axios({ url: `${SETTINGS_SERVICE_BASE_URI}/deleteSettings`, @@ -39,6 +59,12 @@ export const deleteSettings = () => async (dispatch) => { } }; +/** + * Redux thunk action creator to flush the entire database. + * WARNING: This action is destructive and removes all data from the library database. + * + * @returns {Function} Redux thunk function that dispatches SETTINGS_DB_FLUSH_SUCCESS + */ export const flushDb = () => async (dispatch) => { dispatch({ type: SETTINGS_CALL_IN_PROGRESS, @@ -57,6 +83,17 @@ export const flushDb = () => async (dispatch) => { } }; +/** + * Redux thunk action creator to connect and fetch qBittorrent client information. + * Establishes connection to qBittorrent client and retrieves torrent list. + * + * @param {Object} hostInfo - Connection details for qBittorrent + * @param {string} hostInfo.host - qBittorrent server hostname + * @param {number} hostInfo.port - qBittorrent server port + * @param {string} [hostInfo.username] - Authentication username + * @param {string} [hostInfo.password] - Authentication password + * @returns {Function} Redux thunk function that dispatches SETTINGS_QBITTORRENT_TORRENTS_LIST_FETCHED + */ export const getQBitTorrentClientInfo = (hostInfo) => async (dispatch) => { await axios.request({ url: `${QBITTORRENT_SERVICE_BASE_URI}/connect`, @@ -74,4 +111,15 @@ export const getQBitTorrentClientInfo = (hostInfo) => async (dispatch) => { }); }; +/** + * Redux thunk action creator to test Prowlarr connection. + * Verifies connection to Prowlarr indexer management service. + * + * @param {Object} hostInfo - Connection details for Prowlarr + * @param {string} hostInfo.host - Prowlarr server hostname + * @param {number} hostInfo.port - Prowlarr server port + * @param {string} hostInfo.apiKey - Prowlarr API key + * @returns {Function} Redux thunk function (currently not implemented) + * @todo Implement Prowlarr connection verification + */ export const getProwlarrConnectionInfo = (hostInfo) => async (dispatch) => {}; diff --git a/src/client/components/App.tsx b/src/client/components/App.tsx index bbce6c2..3f0def0 100644 --- a/src/client/components/App.tsx +++ b/src/client/components/App.tsx @@ -1,3 +1,10 @@ +/** + * @fileoverview Root application component. + * Provides the main layout structure with navigation, content outlet, + * and toast notifications. Initializes socket connection on mount. + * @module components/App + */ + import React, { ReactElement, useEffect } from "react"; import { Outlet } from "react-router-dom"; import { Navbar2 } from "./shared/Navbar2"; @@ -5,6 +12,26 @@ import { ToastContainer } from "react-toastify"; import "../../app.css"; import { useStore } from "../store"; +/** + * Root application component that provides the main layout structure. + * + * Features: + * - Initializes WebSocket connection to the server on mount + * - Renders the navigation bar across all routes + * - Provides React Router outlet for child routes + * - Includes toast notification container for app-wide notifications + * + * @returns {ReactElement} The root application layout + * @example + * // Used as the root element in React Router configuration + * const router = createBrowserRouter([ + * { + * path: "/", + * element: , + * children: [...] + * } + * ]); + */ export const App = (): ReactElement => { useEffect(() => { useStore.getState().getSocket("/"); // Connect to the base namespace diff --git a/src/client/components/Import/Import.tsx b/src/client/components/Import/Import.tsx index 8db16af..79d7aec 100644 --- a/src/client/components/Import/Import.tsx +++ b/src/client/components/Import/Import.tsx @@ -1,3 +1,10 @@ +/** + * @fileoverview Import page component for managing comic library imports. + * Provides UI for starting imports, monitoring progress, viewing history, + * and handling directory configuration issues. + * @module components/Import/Import + */ + import { ReactElement, useEffect, useRef, useState } from "react"; import { format } from "date-fns"; import { isEmpty } from "lodash"; @@ -10,16 +17,40 @@ import { RealTimeImportStats } from "./RealTimeImportStats"; import { useImportSessionStatus } from "../../hooks/useImportSessionStatus"; import { SETTINGS_SERVICE_BASE_URI } from "../../constants/endpoints"; +/** + * Represents an issue with a configured directory. + * @interface DirectoryIssue + * @property {string} directory - Path to the directory with issues + * @property {string} issue - Description of the issue + */ interface DirectoryIssue { directory: string; issue: string; } +/** + * Result of directory status check from the backend. + * @interface DirectoryStatus + * @property {boolean} isValid - Whether all required directories are accessible + * @property {DirectoryIssue[]} issues - List of specific issues found + */ interface DirectoryStatus { isValid: boolean; issues: DirectoryIssue[]; } +/** + * Import page component for managing comic library imports. + * + * Features: + * - Real-time import progress tracking via WebSocket + * - Directory status validation before import + * - Force re-import functionality for fixing indexing issues + * - Past import history table + * - Session management for import tracking + * + * @returns {ReactElement} The import page UI + */ export const Import = (): ReactElement => { const [importError, setImportError] = useState(null); const queryClient = useQueryClient(); diff --git a/src/client/components/Import/RealTimeImportStats.tsx b/src/client/components/Import/RealTimeImportStats.tsx index dbdf3ad..e830070 100644 --- a/src/client/components/Import/RealTimeImportStats.tsx +++ b/src/client/components/Import/RealTimeImportStats.tsx @@ -1,3 +1,10 @@ +/** + * @fileoverview Real-time import statistics component with live progress tracking. + * Displays import statistics, progress bars, and file detection notifications + * using WebSocket events for real-time updates. + * @module components/Import/RealTimeImportStats + */ + import { ReactElement, useState } from "react"; import { Link } from "react-router-dom"; import { @@ -15,7 +22,7 @@ import { StatsCard } from "../shared/StatsCard"; import { ProgressBar } from "../shared/ProgressBar"; /** - * RealTimeImportStats component displays import statistics with a card-based layout and progress bar. + * Real-time import statistics component with card-based layout and progress tracking. * * This component manages three distinct states: * - **Pre-import (idle)**: Shows current file counts and "Start Import" button when new files exist diff --git a/src/client/components/shared/AlertCard.tsx b/src/client/components/shared/AlertCard.tsx index 34d9e53..1df71d5 100644 --- a/src/client/components/shared/AlertCard.tsx +++ b/src/client/components/shared/AlertCard.tsx @@ -1,5 +1,16 @@ +/** + * @fileoverview Reusable alert card component for displaying status messages. + * Supports multiple variants (error, warning, info, success) with consistent + * styling and optional dismiss functionality. + * @module components/shared/AlertCard + */ + import { ReactElement, ReactNode } from "react"; +/** + * Visual style variants for the alert card. + * @typedef {"error"|"warning"|"info"|"success"} AlertVariant + */ export type AlertVariant = "error" | "warning" | "info" | "success"; interface AlertCardProps { diff --git a/src/client/components/shared/ProgressBar.tsx b/src/client/components/shared/ProgressBar.tsx index e7c5884..ba18cfa 100644 --- a/src/client/components/shared/ProgressBar.tsx +++ b/src/client/components/shared/ProgressBar.tsx @@ -1,5 +1,15 @@ +/** + * @fileoverview Reusable progress bar component with percentage display. + * Supports animated shimmer effect for active states and customizable labels. + * @module components/shared/ProgressBar + */ + import { ReactElement } from "react"; +/** + * Props for the ProgressBar component. + * @interface ProgressBarProps + */ interface ProgressBarProps { /** Current progress value */ current: number; diff --git a/src/client/components/shared/StatsCard.tsx b/src/client/components/shared/StatsCard.tsx index 45a7309..7430a18 100644 --- a/src/client/components/shared/StatsCard.tsx +++ b/src/client/components/shared/StatsCard.tsx @@ -1,5 +1,15 @@ +/** + * @fileoverview Reusable stats card component for displaying numeric metrics. + * Used for dashboards and import statistics displays. + * @module components/shared/StatsCard + */ + import { ReactElement } from "react"; +/** + * Props for the StatsCard component. + * @interface StatsCardProps + */ interface StatsCardProps { /** The main numeric value to display */ value: number; diff --git a/src/client/constants/action-types.ts b/src/client/constants/action-types.ts index c0fb746..24a83fe 100644 --- a/src/client/constants/action-types.ts +++ b/src/client/constants/action-types.ts @@ -1,8 +1,23 @@ +/** + * @fileoverview Redux action type constants for the application. + * Organizes action types by feature/domain for better maintainability. + * @module constants/action-types + */ + +// ============================================================================= +// COMICVINE API ACTION TYPES +// ============================================================================= + +/** @constant {string} CV_API_CALL_IN_PROGRESS - ComicVine API call started */ export const CV_API_CALL_IN_PROGRESS = "CV_SEARCH_IN_PROGRESS"; +/** @constant {string} CV_SEARCH_FAILURE - ComicVine search failed */ export const CV_SEARCH_FAILURE = "CV_SEARCH_FAILURE"; +/** @constant {string} CV_SEARCH_SUCCESS - ComicVine search completed successfully */ export const CV_SEARCH_SUCCESS = "CV_SEARCH_SUCCESS"; +/** @constant {string} CV_CLEANUP - Reset ComicVine state */ export const CV_CLEANUP = "CV_CLEANUP"; +/** @constant {string} CV_API_GENERIC_FAILURE - Generic ComicVine API error */ export const CV_API_GENERIC_FAILURE = "CV_API_GENERIC_FAILURE"; export const IMS_COMICBOOK_METADATA_FETCHED = "IMS_SOCKET_DATA_FETCHED"; diff --git a/src/client/constants/endpoints.ts b/src/client/constants/endpoints.ts index 4bf983b..d550f5e 100644 --- a/src/client/constants/endpoints.ts +++ b/src/client/constants/endpoints.ts @@ -1,3 +1,23 @@ +/** + * @fileoverview API endpoint configuration constants. + * Builds URIs for all microservices used by the application. + * Supports environment-based configuration via Vite environment variables. + * @module constants/endpoints + */ + +/** + * Constructs a full URI from protocol, host, port, and path components. + * + * @param {Record} options - URI component options + * @param {string} options.protocol - Protocol (http, https, ws, wss) + * @param {string} options.host - Hostname or IP address + * @param {string} options.port - Port number + * @param {string} options.apiPath - API path prefix + * @returns {string} Complete URI string + * @example + * hostURIBuilder({ protocol: "http", host: "localhost", port: "3000", apiPath: "/api" }) + * // Returns "http://localhost:3000/api" + */ export const hostURIBuilder = (options: Record): string => { return ( options.protocol + @@ -9,6 +29,14 @@ export const hostURIBuilder = (options: Record): string => { ); }; +// ============================================================================= +// SERVICE ENDPOINT CONSTANTS +// ============================================================================= + +/** + * CORS proxy server URI for bypassing cross-origin restrictions. + * @constant {string} + */ export const CORS_PROXY_SERVER_URI = hostURIBuilder({ protocol: "http", host: import.meta.env.VITE_UNDERLYING_HOSTNAME || "localhost", diff --git a/src/client/graphql/adapters/comicAdapter.ts b/src/client/graphql/adapters/comicAdapter.ts index ea80e04..f43f929 100644 --- a/src/client/graphql/adapters/comicAdapter.ts +++ b/src/client/graphql/adapters/comicAdapter.ts @@ -1,8 +1,41 @@ +/** + * @fileoverview Adapter functions for transforming GraphQL responses to legacy formats. + * Enables gradual migration from REST API to GraphQL while maintaining backward + * compatibility with existing components and data structures. + * @module graphql/adapters/comicAdapter + */ + 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 + * @typedef {Object} LegacyComicFormat + * @property {string} _id - Comic document ID + * @property {Object} rawFileDetails - Original file information + * @property {Object} inferredMetadata - Auto-detected metadata from parsing + * @property {Object} sourcedMetadata - Metadata from external sources (ComicVine, LOCG, etc.) + * @property {Object} acquisition - Download/acquisition tracking data + * @property {string} createdAt - ISO timestamp of creation + * @property {string} updatedAt - ISO timestamp of last update + * @property {Object} __graphql - Original GraphQL response for forward compatibility + */ + +/** + * Transforms a GraphQL Comic query response to the legacy REST API format. + * This adapter enables gradual migration by allowing components to work with + * both new GraphQL data and legacy data structures. + * + * Handles: + * - Parsing stringified JSON in sourcedMetadata fields + * - Building inferredMetadata from canonical metadata as fallback + * - Mapping rawFileDetails to expected structure + * - Preserving original GraphQL data for forward compatibility + * + * @param {GetComicByIdQuery['comic']} graphqlComic - The GraphQL comic response object + * @returns {LegacyComicFormat|null} Transformed comic in legacy format, or null if input is null + * @example + * const { data } = useGetComicByIdQuery({ id: comicId }); + * const legacyComic = adaptGraphQLComicToLegacy(data?.comic); + * // legacyComic now has _id, rawFileDetails, sourcedMetadata, etc. */ export function adaptGraphQLComicToLegacy(graphqlComic: GetComicByIdQuery['comic']) { if (!graphqlComic) return null; diff --git a/src/client/graphql/fetcher.ts b/src/client/graphql/fetcher.ts index 32979a1..345908f 100644 --- a/src/client/graphql/fetcher.ts +++ b/src/client/graphql/fetcher.ts @@ -1,5 +1,32 @@ +/** + * @fileoverview GraphQL fetcher utility for React Query integration. + * Provides a generic fetcher function that handles GraphQL requests + * to the library service backend. + * @module graphql/fetcher + */ + import { LIBRARY_SERVICE_HOST } from '../constants/endpoints'; +/** + * Creates a GraphQL fetcher function for use with React Query. + * Handles POST requests to the GraphQL endpoint with proper error handling. + * + * @template TData - The expected response data type + * @template TVariables - The GraphQL variables type + * @param {string} query - The GraphQL query string + * @param {TVariables} [variables] - Optional query variables + * @param {RequestInit['headers']} [options] - Additional request headers + * @returns {Function} Async function that executes the GraphQL request and returns TData + * @throws {Error} Throws on HTTP errors or GraphQL errors + * @example + * const { data } = useQuery({ + * queryKey: ['comic', id], + * queryFn: fetcher( + * GET_COMIC_QUERY, + * { id } + * ) + * }); + */ export function fetcher( query: string, variables?: TVariables, diff --git a/src/client/hooks/useDarkMode.tsx b/src/client/hooks/useDarkMode.tsx index 0af1c37..f1fa5fa 100644 --- a/src/client/hooks/useDarkMode.tsx +++ b/src/client/hooks/useDarkMode.tsx @@ -1,5 +1,33 @@ +/** + * @fileoverview Custom React hook for managing dark/light theme mode. + * Provides theme persistence using localStorage and applies theme classes + * to the document root element for CSS-based theming. + * @module hooks/useDarkMode + */ + import React, { useEffect, useState } from "react"; +/** + * Custom hook for managing dark mode theme state. + * Persists the user's theme preference to localStorage and applies + * the appropriate CSS class to the document root element. + * + * @returns {[string, React.Dispatch>]} A tuple containing: + * - theme: The current theme value ("dark" or "light") + * - setTheme: State setter function to change the theme + * @example + * const [theme, setTheme] = useDarkMode(); + * + * // Toggle theme + * const toggleTheme = () => { + * setTheme(theme === "dark" ? "light" : "dark"); + * }; + * + * // Use in JSX + * + */ export const useDarkMode = () => { const [theme, setTheme] = useState(localStorage.theme); const colorTheme = theme === "dark" ? "light" : "dark"; diff --git a/src/client/hooks/useImportSessionStatus.ts b/src/client/hooks/useImportSessionStatus.ts index 64e10b7..859f08a 100644 --- a/src/client/hooks/useImportSessionStatus.ts +++ b/src/client/hooks/useImportSessionStatus.ts @@ -1,8 +1,19 @@ +/** + * @fileoverview Custom React hook for tracking import session status. + * Provides real-time import progress monitoring using Socket.IO events + * combined with GraphQL queries for reliable state management. + * @module hooks/useImportSessionStatus + */ + import { useEffect, useState, useCallback, useRef } from "react"; import { useGetActiveImportSessionQuery } from "../graphql/generated"; import { useStore } from "../store"; import { useShallow } from "zustand/react/shallow"; +/** + * Possible states for an import session. + * @typedef {"idle"|"running"|"completed"|"failed"|"unknown"} ImportSessionStatus + */ export type ImportSessionStatus = | "idle" // No import in progress | "running" // Import actively processing @@ -10,6 +21,20 @@ export type ImportSessionStatus = | "failed" // Import finished with errors | "unknown"; // Unable to determine status +/** + * Complete state object representing the current import session. + * @interface ImportSessionState + * @property {ImportSessionStatus} status - Current status of the import session + * @property {string|null} sessionId - Unique identifier for the active session, or null if none + * @property {number} progress - Import progress percentage (0-100) + * @property {Object|null} stats - Detailed file processing statistics + * @property {number} stats.filesQueued - Total files queued for import + * @property {number} stats.filesProcessed - Files that have been processed + * @property {number} stats.filesSucceeded - Files successfully imported + * @property {number} stats.filesFailed - Files that failed to import + * @property {boolean} isComplete - Whether the import session has finished + * @property {boolean} isActive - Whether import is currently processing + */ export interface ImportSessionState { status: ImportSessionStatus; sessionId: string | null; @@ -25,20 +50,32 @@ export interface ImportSessionState { } /** - * Custom hook to definitively determine import session completion status + * Custom hook to definitively determine import session completion status. * - * Uses Socket.IO events to trigger GraphQL refetches: - * - IMPORT_SESSION_STARTED: New import started - * - IMPORT_SESSION_UPDATED: Progress update - * - IMPORT_SESSION_COMPLETED: Import finished - * - LS_IMPORT_QUEUE_DRAINED: All jobs processed + * Uses Socket.IO events to trigger GraphQL refetches for real-time updates: + * - `IMPORT_SESSION_STARTED`: New import started + * - `IMPORT_SESSION_UPDATED`: Progress update + * - `IMPORT_SESSION_COMPLETED`: Import finished + * - `LS_IMPORT_QUEUE_DRAINED`: All jobs processed * * A session is considered DEFINITIVELY COMPLETE when: - * - session.status === "completed" OR session.status === "failed" - * - OR LS_IMPORT_QUEUE_DRAINED event is received AND no active session exists - * - OR IMPORT_SESSION_COMPLETED event is received + * - `session.status === "completed"` OR `session.status === "failed"` + * - OR `LS_IMPORT_QUEUE_DRAINED` event is received AND no active session exists + * - OR `IMPORT_SESSION_COMPLETED` event is received * - * NO POLLING - relies entirely on Socket.IO events for real-time updates + * @returns {ImportSessionState} Current state of the import session including + * status, progress, and file statistics + * @example + * const { status, progress, stats, isComplete, isActive } = useImportSessionStatus(); + * + * if (isActive) { + * console.log(`Import progress: ${progress}%`); + * console.log(`Files processed: ${stats?.filesProcessed}/${stats?.filesQueued}`); + * } + * + * if (isComplete && status === "completed") { + * console.log("Import finished successfully!"); + * } */ export const useImportSessionStatus = (): ImportSessionState => { diff --git a/src/client/hooks/useImportSocketEvents.ts b/src/client/hooks/useImportSocketEvents.ts index 985bd63..50dd8b6 100644 --- a/src/client/hooks/useImportSocketEvents.ts +++ b/src/client/hooks/useImportSocketEvents.ts @@ -1,8 +1,23 @@ +/** + * @fileoverview Custom React hook for managing real-time import events via Socket.IO. + * Provides reactive state tracking for import progress, file detection notifications, + * and automatic query invalidation when import status changes. + * @module hooks/useImportSocketEvents + */ + import { useEffect, useState, useCallback } from "react"; import { useQueryClient } from "@tanstack/react-query"; import { useStore } from "../store"; import { useShallow } from "zustand/react/shallow"; +/** + * State object tracking real-time import progress via socket events. + * @interface SocketImportState + * @property {boolean} active - Whether import is currently in progress + * @property {number} completed - Number of successfully completed import jobs + * @property {number} total - Total number of jobs in the import queue + * @property {number} failed - Number of failed import jobs + */ export interface SocketImportState { /** Whether import is currently in progress */ active: boolean; @@ -14,6 +29,13 @@ export interface SocketImportState { failed: number; } +/** + * Return type for the useImportSocketEvents hook. + * @interface UseImportSocketEventsReturn + * @property {SocketImportState|null} socketImport - Real-time import progress state, null when no import active + * @property {string|null} detectedFile - Name of recently detected file for toast notification + * @property {Function} clearDetectedFile - Callback to clear the detected file notification + */ export interface UseImportSocketEventsReturn { /** Real-time import progress state tracked via socket events */ socketImport: SocketImportState | null; @@ -25,13 +47,28 @@ export interface UseImportSocketEventsReturn { /** * Custom hook that manages socket event subscriptions for import-related events. - * - * Subscribes to: + * Automatically handles subscription lifecycle and query cache invalidation. + * + * Subscribes to the following Socket.IO events: * - `LS_LIBRARY_STATS` / `LS_FILES_MISSING`: Triggers statistics refresh * - `LS_FILE_DETECTED`: Shows toast notification for newly detected files * - `LS_INCREMENTAL_IMPORT_STARTED`: Initializes progress tracking * - `LS_COVER_EXTRACTED` / `LS_COVER_EXTRACTION_FAILED`: Updates progress counts * - `LS_IMPORT_QUEUE_DRAINED`: Marks import as complete + * + * @returns {UseImportSocketEventsReturn} Object containing import state and notification handlers + * @example + * const { socketImport, detectedFile, clearDetectedFile } = useImportSocketEvents(); + * + * // Show progress bar when import is active + * {socketImport?.active && ( + * + * )} + * + * // Show toast when file detected + * {detectedFile && ( + * + * )} */ export function useImportSocketEvents(): UseImportSocketEventsReturn { const [socketImport, setSocketImport] = useState(null); diff --git a/src/client/services/DcppSearchService.ts b/src/client/services/DcppSearchService.ts index 049cd3c..f1a6f47 100644 --- a/src/client/services/DcppSearchService.ts +++ b/src/client/services/DcppSearchService.ts @@ -1,6 +1,38 @@ +/** + * @fileoverview AirDC++ WebSocket service for Direct Connect protocol communication. + * Provides a configured socket connection to the AirDC++ daemon for search + * and download operations. + * @module services/DcppSearchService + */ + import { Socket } from "airdcpp-apisocket"; +/** + * Factory class for creating configured AirDC++ WebSocket connections. + * Wraps the airdcpp-apisocket library with application-specific configuration. + * + * @class AirDCPPSocket + * @example + * const socket = new AirDCPPSocket({ + * protocol: "https", + * hostname: "localhost:5600", + * username: "admin", + * password: "password" + * }); + * await socket.connect(); + */ class AirDCPPSocket { + /** + * Creates a new AirDC++ socket connection instance. + * + * @constructor + * @param {Object} configuration - Connection configuration object + * @param {string} configuration.protocol - HTTP protocol ("http" or "https") + * @param {string} configuration.hostname - Server hostname with optional port + * @param {string} configuration.username - AirDC++ username for authentication + * @param {string} configuration.password - AirDC++ password for authentication + * @returns {Socket} Configured AirDC++ socket instance + */ constructor(configuration) { let socketProtocol = ""; if (configuration.protocol === "https") { diff --git a/src/client/services/api/SearchApi.ts b/src/client/services/api/SearchApi.ts index ea9cf84..7e70794 100644 --- a/src/client/services/api/SearchApi.ts +++ b/src/client/services/api/SearchApi.ts @@ -1,3 +1,10 @@ +/** + * @fileoverview Search API service for AirDC++ Direct Connect searches. + * Provides high-level search functionality by wrapping the socket service + * and managing search instances and result collection. + * @module services/api/SearchApi + */ + import SocketService from "../DcppSearchService"; import { SearchQuery, @@ -7,16 +14,46 @@ import { } from "threetwo-ui-typings"; import SearchConstants from "../../constants/search.constants"; +/** + * Configuration data for initiating an AirDC++ search. + * @interface SearchData + * @property {Object} query - Search query configuration + * @property {string} query.pattern - Required search pattern/term + * @property {string[]|undefined|null} hub_urls - List of hub URLs to search, or null for all hubs + * @property {PriorityEnum} priority - Download priority for matched results + */ interface SearchData { query: Pick & Partial>; hub_urls: string[] | undefined | null; priority: PriorityEnum; } +/** + * Pauses execution for a specified duration. + * + * @private + * @param {number} ms - Duration to sleep in milliseconds + * @returns {Promise} Promise that resolves after the specified delay + */ function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } +/** + * Initiates a search on the AirDC++ network through connected hubs. + * Creates a new search instance, registers listeners for results, + * and sends the search query to the specified hubs. + * + * @async + * @param {SearchData} data - Search configuration data + * @returns {Promise} Search queue information from the server + * @example + * const queueInfo = await search({ + * query: { pattern: "Batman #1" }, + * hub_urls: null, // Search all connected hubs + * priority: PriorityEnum.Normal + * }); + */ export const search = async (data: SearchData) => { await SocketService.connect(); const instance: SearchInstance = await SocketService.post("search"); @@ -36,6 +73,17 @@ export const search = async (data: SearchData) => { return searchQueueInfo; }; +/** + * Callback handler for when a search query has been sent to hubs. + * Waits for results to accumulate, then processes the top matches. + * + * @private + * @async + * @param {SearchData} item - The original search data + * @param {SearchInstance} instance - The search instance object + * @param {Function} unsubscribe - Cleanup function to remove the event listener + * @param {Object} searchInfo - Information about the sent search + */ const onSearchSent = async (item, instance, unsubscribe, searchInfo) => { // Collect the results for 5 seconds await sleep(5000); diff --git a/src/client/shared/utils/formatting.utils.ts b/src/client/shared/utils/formatting.utils.ts index 3bbf167..e90206b 100644 --- a/src/client/shared/utils/formatting.utils.ts +++ b/src/client/shared/utils/formatting.utils.ts @@ -1,3 +1,18 @@ +/** + * @fileoverview Utility functions for string formatting and text manipulation. + * @module shared/utils/formatting + */ + +/** + * Removes a leading period character from a string if present. + * Useful for normalizing file extensions or dotfile names. + * + * @param {string} input - The input string to process + * @returns {string} The input string with the leading period removed, if one existed + * @example + * removeLeadingPeriod(".txt") // returns "txt" + * removeLeadingPeriod("txt") // returns "txt" + */ export const removeLeadingPeriod = (input: string): string => { if (input.charAt(0) == ".") { input = input.substr(1); @@ -5,10 +20,37 @@ export const removeLeadingPeriod = (input: string): string => { return input; }; +/** + * Escapes pound/hash symbols (#) in a string by replacing them with URL-encoded equivalent (%23). + * Useful for preparing strings to be used in URLs or file paths. + * + * @param {string} input - The input string containing pound symbols to escape + * @returns {string} The input string with all # characters replaced with %23 + * @example + * escapePoundSymbol("Issue #123") // returns "Issue %23123" + */ export const escapePoundSymbol = (input: string): string => { return input.replace(/\#/gm, "%23"); }; +/** + * Represents a comic book document with various metadata sources. + * Used for displaying comic information across the application. + * + * @interface ComicBookDocument + * @property {string} id - Unique identifier for the comic book document + * @property {Object} [canonicalMetadata] - User-verified or authoritative metadata + * @property {Object} [canonicalMetadata.series] - Series information + * @property {string|null} [canonicalMetadata.series.value] - Series name + * @property {Object} [canonicalMetadata.issueNumber] - Issue number information + * @property {string|number|null} [canonicalMetadata.issueNumber.value] - Issue number + * @property {Object} [inferredMetadata] - Automatically detected metadata from file parsing + * @property {Object} [inferredMetadata.issue] - Issue information + * @property {string|null} [inferredMetadata.issue.name] - Inferred series/issue name + * @property {string|number|null} [inferredMetadata.issue.number] - Inferred issue number + * @property {Object} [rawFileDetails] - Original file information + * @property {string|null} [rawFileDetails.name] - Original filename + */ interface ComicBookDocument { id: string; canonicalMetadata?: { @@ -27,8 +69,17 @@ interface ComicBookDocument { * Generates a display label for a comic book from its metadata. * Prioritizes canonical metadata, falls back to inferred, then raw file name. * - * @param comic - The comic book document object - * @returns A formatted string like "Series Name #123" or the file name as fallback + * @param {ComicBookDocument} comic - The comic book document object + * @returns {string} A formatted string like "Series Name #123" or the file name as fallback + * @example + * // With full canonical metadata + * getComicDisplayLabel({ id: "1", canonicalMetadata: { series: { value: "Batman" }, issueNumber: { value: 42 } } }) + * // returns "Batman #42" + * + * @example + * // Fallback to raw file name + * getComicDisplayLabel({ id: "1", rawFileDetails: { name: "batman-042.cbz" } }) + * // returns "batman-042.cbz" */ export const getComicDisplayLabel = (comic: ComicBookDocument): string => { const series = diff --git a/src/client/shared/utils/i18n.util.ts b/src/client/shared/utils/i18n.util.ts index f08e4a9..8c73c05 100644 --- a/src/client/shared/utils/i18n.util.ts +++ b/src/client/shared/utils/i18n.util.ts @@ -1,23 +1,49 @@ -// i18n.js +/** + * @fileoverview Internationalization (i18n) configuration module. + * Sets up i18next with HTTP backend for loading translations, + * automatic language detection, and React integration. + * @module shared/utils/i18n + * @see {@link https://www.i18next.com/overview/configuration-options i18next Configuration Options} + */ + import i18n from "i18next"; import { initReactI18next } from "react-i18next"; import HttpBackend from "i18next-http-backend"; import LanguageDetector from "i18next-browser-languagedetector"; +/** + * Configured i18next instance for the application. + * + * Features: + * - HTTP backend for loading translation files + * - Automatic browser language detection + * - React-i18next integration for hooks and components + * - English as default and fallback language + * + * @type {import("i18next").i18n} + * @example + * // Using in a component + * import { useTranslation } from 'react-i18next'; + * const { t } = useTranslation(); + * return {t('key.name')}; + */ i18n - // Learn more about options: https://www.i18next.com/overview/configuration-options - .use(HttpBackend) // Load translations over http - .use(LanguageDetector) // Detect language automatically - .use(initReactI18next) // Pass i18n instance to react-i18next + .use(HttpBackend) + .use(LanguageDetector) + .use(initReactI18next) .init({ - lng: "en", // Specify the language + /** @type {string} Default language code */ + lng: "en", + /** @type {string} Fallback language when translation is missing */ fallbackLng: "en", + /** @type {boolean} Enable debug mode for development */ debug: true, interpolation: { - escapeValue: false, // Not needed for React + /** @type {boolean} Disable escaping since React handles XSS protection */ + escapeValue: false, }, backend: { - // path where resources get loaded from + /** @type {string} Path to translation JSON files */ loadPath: "./src/client/locales/en/translation.json", }, }); diff --git a/src/client/shared/utils/metadata.utils.ts b/src/client/shared/utils/metadata.utils.ts index 10b4e26..d69884c 100644 --- a/src/client/shared/utils/metadata.utils.ts +++ b/src/client/shared/utils/metadata.utils.ts @@ -1,17 +1,59 @@ +/** + * @fileoverview Utility functions for handling comic metadata from various sources. + * Provides functions to determine cover images and external metadata from + * sources like ComicVine, League of Comic Geeks (LOCG), and raw file details. + * @module shared/utils/metadata + */ + import { filter, isEmpty, isNil, isUndefined, min, minBy } from "lodash"; import { LIBRARY_SERVICE_HOST } from "../../constants/endpoints"; import { escapePoundSymbol } from "./formatting.utils"; +/** + * @typedef {Object} CoverFileEntry + * @property {string} objectReference - Reference key to the source object + * @property {number} priority - Priority level for cover selection (lower = higher priority) + * @property {string} url - URL to the cover image + * @property {string} issueName - Name of the comic issue + * @property {string} publisher - Publisher name + */ + +/** + * @typedef {Object} ComicMetadataPayload + * @property {Object} [rawFileDetails] - Raw file information from the filesystem + * @property {Object} [rawFileDetails.cover] - Cover image details + * @property {string} [rawFileDetails.cover.filePath] - Path to the cover file + * @property {string} [rawFileDetails.name] - File name + * @property {Object} [wanted] - Wanted list metadata + * @property {Object} [comicInfo] - ComicInfo.xml metadata + * @property {Object} [comicvine] - ComicVine API metadata + * @property {Object} [comicvine.image] - Image information + * @property {string} [comicvine.image.small_url] - Small cover image URL + * @property {string} [comicvine.name] - Issue name from ComicVine + * @property {Object} [comicvine.publisher] - Publisher information + * @property {string} [comicvine.publisher.name] - Publisher name + * @property {Object} [locg] - League of Comic Geeks metadata + * @property {string} [locg.cover] - Cover image URL + * @property {string} [locg.name] - Issue name + * @property {string} [locg.publisher] - Publisher name + */ + +/** + * Determines the best available cover file from multiple metadata sources. + * Evaluates sources in priority order: rawFileDetails (1), wanted (2), comicvine (3), locg (4). + * Returns the highest priority source that has a valid cover URL. + * + * @param {ComicMetadataPayload} data - The comic metadata object containing multiple sources + * @returns {CoverFileEntry} The cover file entry with the highest priority that has a URL, + * or the rawFile entry if no covers are available + * @example + * const cover = determineCoverFile({ + * rawFileDetails: { name: "Batman #1.cbz", cover: { filePath: "covers/batman-1.jpg" } }, + * comicvine: { image: { small_url: "https://comicvine.com/..." }, name: "Batman" } + * }); + * // Returns rawFileDetails cover (priority 1) if available + */ export const determineCoverFile = (data): any => { - /* For a payload like this: - const foo = { - rawFileDetails: {}, // #1 - wanted: {}, - comicInfo: {}, - comicvine: {}, // #2 - locg: {}, // #3 - }; - */ const coverFile = { rawFile: { objectReference: "rawFileDetails", @@ -42,13 +84,15 @@ export const determineCoverFile = (data): any => { publisher: "", }, }; - // comicvine + + // Extract ComicVine metadata if (!isEmpty(data.comicvine)) { coverFile.comicvine.url = data?.comicvine?.image?.small_url; coverFile.comicvine.issueName = data.comicvine?.name; coverFile.comicvine.publisher = data.comicvine?.publisher?.name; } - // rawFileDetails + + // Extract raw file details if (!isEmpty(data.rawFileDetails) && data.rawFileDetails.cover?.filePath) { const encodedFilePath = encodeURI( `${LIBRARY_SERVICE_HOST}/${data.rawFileDetails.cover.filePath}`, @@ -58,8 +102,8 @@ export const determineCoverFile = (data): any => { } else if (!isEmpty(data.rawFileDetails)) { coverFile.rawFile.issueName = data.rawFileDetails.name; } - // wanted + // Extract League of Comic Geeks metadata if (!isNil(data.locg)) { coverFile.locg.url = data.locg.cover; coverFile.locg.issueName = data.locg.name; @@ -79,6 +123,28 @@ export const determineCoverFile = (data): any => { return coverFile.rawFile; }; +/** + * @typedef {Object} ExternalMetadataResult + * @property {string} coverURL - URL to the cover image + * @property {string} issue - Issue name or title + * @property {string} icon - Icon filename for the metadata source + */ + +/** + * Extracts external metadata from a specific source. + * Supports ComicVine and League of Comic Geeks (LOCG) as metadata sources. + * + * @param {string} metadataSource - The source identifier ("comicvine" or "locg") + * @param {ComicMetadataPayload} source - The comic metadata object + * @returns {ExternalMetadataResult|Object|null} The extracted metadata with cover URL, issue name, + * and source icon; empty object for undefined source; + * null if source data is nil + * @example + * const metadata = determineExternalMetadata("comicvine", { + * comicvine: { image: { small_url: "https://..." }, name: "Batman #1" } + * }); + * // Returns { coverURL: "https://...", issue: "Batman #1", icon: "cvlogo.svg" } + */ export const determineExternalMetadata = ( metadataSource: string, source: any, diff --git a/src/client/shared/utils/object.utils.ts b/src/client/shared/utils/object.utils.ts index 7a43562..4ee9109 100644 --- a/src/client/shared/utils/object.utils.ts +++ b/src/client/shared/utils/object.utils.ts @@ -1,15 +1,38 @@ +/** + * @fileoverview Utility functions for deep object comparison and traversal. + * Provides tools for finding differences between objects and recursively + * traversing nested object structures. + * @module shared/utils/object + */ + import { transform, isEqual, isObject } from "lodash"; /** - * Deep diff between two object, using lodash - * @param {Object} object Object compared - * @param {Object} base Object to compare with - * @return {Object} Return a new object who represent the diff + * Calculates the deep difference between two objects. + * Returns a new object containing only the properties that differ between + * the compared object and the base object. + * + * @param {Object} object - The object to compare + * @param {Object} base - The base object to compare against + * @returns {Object} A new object representing the differences, where each key + * contains the value from `object` that differs from `base` + * @example + * const obj1 = { a: 1, b: { c: 2, d: 3 } }; + * const obj2 = { a: 1, b: { c: 2, d: 4 } }; + * difference(obj1, obj2); // returns { b: { d: 3 } } */ export const difference = (object, base) => { return changes(object, base); }; +/** + * Internal recursive function that computes changes between two objects. + * + * @private + * @param {Object} object - The object to compare + * @param {Object} base - The base object to compare against + * @returns {Object} Object containing the differences + */ const changes = (object, base) => { return transform(object, (result, value, key) => { if (!isEqual(value, base[key])) { @@ -21,6 +44,17 @@ const changes = (object, base) => { }); }; +/** + * Callback function type for object traversal. + * Called for each property encountered during traversal. + * + * @template T - The type of the object being traversed + * @callback TraverseFunction + * @param {T} obj - The root object being traversed + * @param {string} prop - The current property name + * @param {unknown} value - The value of the current property + * @param {string[]} scope - Array of property names representing the path to current property + */ export type TraverseFunction = ( obj: T, prop: string, @@ -29,16 +63,39 @@ export type TraverseFunction = ( ) => void; /** - * Deep diff between two object, using lodash - * @param {Object} object Object to traverse - * @param {Object} fn Callback function - * @return {Object} Return a new object who represent the diff + * Recursively traverses all properties of an object, invoking a callback + * for each property encountered. Useful for deep inspection or transformation + * of nested object structures. + * + * @template T - The type of the object being traversed + * @param {T} object - The object to traverse + * @param {TraverseFunction} fn - Callback function invoked for each property + * @returns {void} + * @example + * const obj = { a: 1, b: { c: 2 } }; + * traverseObject(obj, (obj, prop, value, scope) => { + * console.log(`${scope.join('.')}.${prop} = ${value}`); + * }); + * // Logs: + * // ".a = 1" + * // ".b = [object Object]" + * // "b.c = 2" */ export const traverseObject = >( object: T, fn: TraverseFunction, ): void => traverseInternal(object, fn, []); +/** + * Internal recursive implementation for object traversal. + * + * @private + * @template T - The type of the object being traversed + * @param {T} object - The object to traverse + * @param {TraverseFunction} fn - Callback function invoked for each property + * @param {string[]} [scope=[]] - Current path scope in the object hierarchy + * @returns {void} + */ const traverseInternal = >( object: T, fn: TraverseFunction, diff --git a/src/client/shared/utils/tradepaperback.utils.ts b/src/client/shared/utils/tradepaperback.utils.ts index 57781c8..dea9727 100644 --- a/src/client/shared/utils/tradepaperback.utils.ts +++ b/src/client/shared/utils/tradepaperback.utils.ts @@ -1,12 +1,47 @@ +/** + * @fileoverview Utility functions for detecting comic book issue types. + * Analyzes deck/description text to identify trade paperbacks, mini-series, + * and other collected edition formats. + * @module shared/utils/tradepaperback + */ + import { flatten, compact, map, isEmpty, isNil } from "lodash"; import axios from "axios"; +/** + * @typedef {Object} IssueTypeMatcher + * @property {RegExp[]} regex - Array of regex patterns to match against the deck text + * @property {string} displayName - Human-readable name for the issue type + */ + +/** + * @typedef {Object} IssueTypeMatch + * @property {string} displayName - The display name of the matched issue type + * @property {string[]} results - Array of matched strings from the deck text + */ + +/** + * Detects the type of comic issue from its deck/description text. + * Identifies formats like Trade Paperbacks (TPB), hardcover collections, + * and mini-series based on keyword patterns. + * + * @param {string} deck - The deck or description text to analyze + * @returns {IssueTypeMatch|undefined} The first matched issue type with its display name + * and matched strings, or undefined if no match + * @example + * detectIssueTypes("Collects issues #1-6 in trade paperback format") + * // returns { displayName: "Trade Paperback", results: ["trade paperback"] } + * + * @example + * detectIssueTypes("A 4-issue mini-series") + * // returns { displayName: "Mini-Series", results: ["mini-series"] } + */ export const detectIssueTypes = (deck: string): any => { const issueTypeMatchers = [ { regex: [ /((trade)?\s?(paperback)|(tpb))/gim, // https://regex101.com/r/FhuowT/1 - /(hard\s?cover)\s?(collect((ion)|(ed)|(ing)))/gim, //https://regex101.com/r/eFJVRM/1 + /(hard\s?cover)\s?(collect((ion)|(ed)|(ing)))/gim, // https://regex101.com/r/eFJVRM/1 ], displayName: "Trade Paperback", }, @@ -20,6 +55,16 @@ export const detectIssueTypes = (deck: string): any => { return compact(matches)[0]; }; +/** + * Tests a deck string against regex patterns and returns match info if found. + * + * @private + * @param {string} deck - The deck or description text to search + * @param {RegExp[]} regexPattern - Array of regex patterns to test against + * @param {string} displayName - The display name to return if a match is found + * @returns {IssueTypeMatch|undefined} Object with displayName and results if matched, + * otherwise undefined + */ const getIssueTypeDisplayName = ( deck: string, regexPattern: RegExp[], diff --git a/src/client/shared/utils/validator.utils.ts b/src/client/shared/utils/validator.utils.ts index 453a94c..2285d96 100644 --- a/src/client/shared/utils/validator.utils.ts +++ b/src/client/shared/utils/validator.utils.ts @@ -1,7 +1,26 @@ +/** + * @fileoverview Validation utility functions for form inputs and data. + * Provides validators for hostnames, URLs, and other common input types. + * @module shared/utils/validator + */ + import { isNil, isUndefined } from "lodash"; +/** + * Validates a hostname string against RFC 1123 standards. + * Hostnames must start and end with alphanumeric characters and can contain + * hyphens. Each label (segment between dots) can be 1-63 characters. + * + * @param {string} hostname - The hostname string to validate + * @returns {string|undefined} undefined if valid, error message string if invalid + * @see {@link https://stackoverflow.com/a/3824105/656708 Hostname validation regex} + * @example + * hostNameValidator("example.com") // returns undefined (valid) + * hostNameValidator("my-server.local") // returns undefined (valid) + * hostNameValidator("-invalid.com") // returns "Enter a valid hostname" + * hostNameValidator("invalid-.com") // returns "Enter a valid hostname" + */ export const hostNameValidator = (hostname: string): string | undefined => { - // https://stackoverflow.com/a/3824105/656708 const hostnameRegex = /^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]))*$/; diff --git a/src/client/store/index.ts b/src/client/store/index.ts index aa3c3a0..c41f1da 100644 --- a/src/client/store/index.ts +++ b/src/client/store/index.ts @@ -1,3 +1,10 @@ +/** + * @fileoverview Global Zustand store for application state management. + * Manages socket connections, import job tracking, and ComicVine scraping state. + * Uses Zustand for lightweight, hook-based state management with React. + * @module store + */ + import { create } from "zustand"; import io, { Socket } from "socket.io-client"; import { SOCKET_BASE_URI } from "../constants/endpoints"; @@ -5,10 +12,17 @@ import { QueryClient } from "@tanstack/react-query"; import { toast } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; +/** + * React Query client instance for managing server state. + * @constant {QueryClient} + */ const queryClient = new QueryClient(); /** - * Global application state interface + * Global application state interface. + * Defines the shape of the Zustand store including socket management, + * import job tracking, and external service state. + * @interface StoreState */ interface StoreState { /** Active socket.io connections by namespace */ diff --git a/src/client/types/index.d.ts b/src/client/types/index.d.ts index 3dd9eb9..f0f48b2 100644 --- a/src/client/types/index.d.ts +++ b/src/client/types/index.d.ts @@ -1,13 +1,55 @@ +/** + * @fileoverview TypeScript type declarations for module imports. + * Provides type definitions for importing static assets and stylesheets + * that aren't natively supported by TypeScript. + * @module types + */ + +/** + * Module declaration for PNG image imports. + * Allows importing .png files as string URLs. + * @example + * import logo from './assets/logo.png'; + * // logo is typed as string + */ declare module "*.png" { const value: string; export = value; } +/** + * Module declaration for JPG image imports. + * Allows importing .jpg files. + */ declare module "*.jpg"; + +/** + * Module declaration for GIF image imports. + * Allows importing .gif files. + */ declare module "*.gif"; + +/** + * Module declaration for LESS stylesheet imports. + * Allows importing .less files. + */ declare module "*.less"; + +/** + * Module declaration for SCSS stylesheet imports. + * Allows importing .scss files. + */ declare module "*.scss"; + +/** + * Module declaration for CSS stylesheet imports. + * Allows importing .css files. + */ declare module "*.css"; -// Comic types are now generated from GraphQL schema -// Import from '../../graphql/generated' instead +/** + * @note Comic types are now generated from GraphQL schema. + * Import from '../../graphql/generated' instead of defining types here. + * The generated types provide full type safety for GraphQL queries and mutations. + * @see {@link module:graphql/generated} + */