JsDoc across all project files

This commit is contained in:
Rishi Ghan
2026-04-15 11:31:52 -04:00
parent 6deab0b87e
commit 2dc38b6c95
28 changed files with 999 additions and 50 deletions

View File

@@ -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<SearchQuery, "pattern"> & Partial<Omit<SearchQuery, "pattern">>;
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<NodeJS.Timeout>} Promise that resolves after the delay
* @example
* await sleep(1000); // Wait 1 second
*/
export const sleep = (ms: number): Promise<NodeJS.Timeout> => {
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 {

View File

@@ -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<string>} comicBookObjectIds - Array of database IDs
* @returns {Function} Redux thunk function that dispatches IMS_COMIC_BOOKS_DB_OBJECTS_FETCHED
*/
export const getComicBooksDetailsByIds =
(comicBookObjectIds: Array<string>) => 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({

View File

@@ -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({

View File

@@ -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<MetronResourceResult>} 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`,

View File

@@ -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) => {};

View File

@@ -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: <App />,
* children: [...]
* }
* ]);
*/
export const App = (): ReactElement => {
useEffect(() => {
useStore.getState().getSocket("/"); // Connect to the base namespace

View File

@@ -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<string | null>(null);
const queryClient = useQueryClient();

View File

@@ -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

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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";

View File

@@ -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<string, string>} 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, string>): string => {
return (
options.protocol +
@@ -9,6 +29,14 @@ export const hostURIBuilder = (options: Record<string, string>): 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",

View File

@@ -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;

View File

@@ -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<GetComicQuery, GetComicQueryVariables>(
* GET_COMIC_QUERY,
* { id }
* )
* });
*/
export function fetcher<TData, TVariables>(
query: string,
variables?: TVariables,

View File

@@ -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<React.SetStateAction<string>>]} 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
* <button onClick={toggleTheme}>
* Current theme: {theme}
* </button>
*/
export const useDarkMode = () => {
const [theme, setTheme] = useState(localStorage.theme);
const colorTheme = theme === "dark" ? "light" : "dark";

View File

@@ -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 => {

View File

@@ -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 && (
* <ProgressBar value={socketImport.completed} max={socketImport.total} />
* )}
*
* // Show toast when file detected
* {detectedFile && (
* <Toast message={`Detected: ${detectedFile}`} onClose={clearDetectedFile} />
* )}
*/
export function useImportSocketEvents(): UseImportSocketEventsReturn {
const [socketImport, setSocketImport] = useState<SocketImportState | null>(null);

View File

@@ -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") {

View File

@@ -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<SearchQuery, "pattern"> & Partial<Omit<SearchQuery, "pattern">>;
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<NodeJS.Timeout> {
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<Object>} 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);

View File

@@ -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 =

View File

@@ -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 <span>{t('key.name')}</span>;
*/
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",
},
});

View File

@@ -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,

View File

@@ -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<T> = (
obj: T,
prop: string,
@@ -29,16 +63,39 @@ export type TraverseFunction<T> = (
) => 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<T>} 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 = <T = Record<string, unknown>>(
object: T,
fn: TraverseFunction<T>,
): 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<T>} fn - Callback function invoked for each property
* @param {string[]} [scope=[]] - Current path scope in the object hierarchy
* @returns {void}
*/
const traverseInternal = <T = Record<string, unknown>>(
object: T,
fn: TraverseFunction<T>,

View File

@@ -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[],

View File

@@ -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]))*$/;

View File

@@ -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 */

View File

@@ -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}
*/