Compare commits

..

15 Commits

Author SHA1 Message Date
463b67cb5b Merge branch 'main' into comicvine-integration-improvements 2024-05-11 15:03:10 -04:00
80746b0b0c 🏗️ Modified the Dockerfile 2024-05-11 15:01:26 -04:00
df835cad7e 🏗️ Automatic downloads WIP 2024-05-09 13:59:46 -04:00
3f593b9efd 🔧 Refactored AirDC++ init in store 2024-04-23 22:45:29 -05:00
8dacff7850 🔧 Refactoring DC++ search/download 2024-04-17 21:14:07 -05:00
5f2500552c 🔧 Refactored Wanted component
Included # of issues in a wanted volume
2024-04-16 22:40:48 -05:00
1de01e7dbd 🔍 CV search metadata wrangling 2024-04-14 00:25:17 -04:00
6de8517bb5 🌍 Added i18n lib 2024-04-11 09:58:53 -04:00
0024660f2e 🔍 Refining CV search UX 2024-04-10 22:35:25 -04:00
8fe49f7034 🔍 Improvements to CV search results 2024-04-09 17:38:14 -04:00
9315ad7454 🍇 Added some integration for issues 2024-04-08 13:30:31 -04:00
e54f997972 Added status checks 2024-04-04 06:39:07 -05:00
ca8c5dcf5b 📚 Wired up story arc fetching 2024-04-04 06:28:14 -05:00
5aecd66abb 🎨 Added some icons to tabs 2024-04-03 21:50:14 -05:00
64a4cfc8eb ️ Refactored VolumeDetail page to use react-query 2024-04-03 13:16:19 -05:00
29 changed files with 3482 additions and 3851 deletions

View File

@@ -12,8 +12,7 @@ RUN apk --no-cache add g++ make libpng-dev git python3 autoconf automake libjpeg
# Install node modules # Install node modules
RUN yarn install --ignore-engines RUN yarn install --ignore-engines
# Explicitly install sass
RUN yarn add -D sass
# Copy the rest of the application # Copy the rest of the application
COPY . . COPY . .

View File

@@ -29,14 +29,14 @@
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"airdcpp-apisocket": "^2.5.0-beta.2", "airdcpp-apisocket": "^2.5.0-beta.2",
"axios": "^1.7.4", "axios": "^1.6.8",
"axios-cache-interceptor": "^1.0.1", "axios-cache-interceptor": "^1.0.1",
"axios-rate-limit": "^1.3.0", "axios-rate-limit": "^1.3.0",
"babel-plugin-styled-components": "^2.1.4", "babel-plugin-styled-components": "^2.1.4",
"date-fns": "^2.28.0", "date-fns": "^2.28.0",
"dayjs": "^1.10.6", "dayjs": "^1.10.6",
"ellipsize": "^0.5.1", "ellipsize": "^0.5.1",
"express": "^4.20.0", "express": "^4.19.2",
"filename-parser": "^1.0.2", "filename-parser": "^1.0.2",
"final-form": "^4.20.2", "final-form": "^4.20.2",
"final-form-arrays": "^3.0.2", "final-form-arrays": "^3.0.2",
@@ -53,27 +53,30 @@
"pretty-bytes": "^5.6.0", "pretty-bytes": "^5.6.0",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"qs": "^6.10.5", "qs": "^6.10.5",
"react": "^19.0.0", "react": "^18.2.0",
"react-collapsible": "^2.9.0", "react-collapsible": "^2.9.0",
"react-comic-viewer": "^0.4.0", "react-comic-viewer": "^0.4.0",
"react-day-picker": "^8.10.0", "react-day-picker": "^8.10.0",
"react-dom": "^19.0.0", "react-dom": "^18.2.0",
"react-fast-compare": "^3.2.0", "react-fast-compare": "^3.2.0",
"react-final-form": "^6.5.9", "react-final-form": "^6.5.9",
"react-final-form-arrays": "^3.1.4", "react-final-form-arrays": "^3.1.4",
"react-i18next": "^14.1.0", "react-i18next": "^14.1.0",
"react-loader-spinner": "^4.0.0", "react-loader-spinner": "^4.0.0",
"react-modal": "^3.15.1", "react-modal": "^3.15.1",
"react-router": "^7.1.5", "react-router": "^6.9.0",
"react-router-dom": "^6.9.0",
"react-select": "^5.8.0", "react-select": "^5.8.0",
"react-select-async-paginate": "^0.7.2", "react-select-async-paginate": "^0.7.2",
"react-sliding-pane": "^7.1.0", "react-sliding-pane": "^7.1.0",
"react-textarea-autosize": "^8.3.4", "react-textarea-autosize": "^8.3.4",
"react-toastify": "^10.0.5", "reapop": "^4.2.1",
"socket.io-client": "^4.3.2", "socket.io-client": "^4.3.2",
"styled-components": "^6.1.0", "styled-components": "^6.1.0",
"threetwo-ui-typings": "^1.0.14", "threetwo-ui-typings": "^1.0.14",
"vite": "^5.4.12",
"vite": "^5.2.7",
"vite-plugin-html": "^3.2.0", "vite-plugin-html": "^3.2.0",
"websocket": "^1.0.34", "websocket": "^1.0.34",
"zustand": "^4.4.6" "zustand": "^4.4.6"
@@ -97,8 +100,8 @@
"@types/jest": "^26.0.20", "@types/jest": "^26.0.20",
"@types/lodash": "^4.14.168", "@types/lodash": "^4.14.168",
"@types/node": "^14.14.34", "@types/node": "^14.14.34",
"@types/react": "^19.0.0", "@types/react": "^18.0.28",
"@types/react-dom": "^19.0.0", "@types/react-dom": "^18.0.11",
"@types/react-redux": "^7.1.25", "@types/react-redux": "^7.1.25",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
@@ -112,7 +115,7 @@
"eslint-plugin-prettier": "^3.3.1", "eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-react": "^7.22.0", "eslint-plugin-react": "^7.22.0",
"eslint-plugin-storybook": "^0.6.13", "eslint-plugin-storybook": "^0.6.13",
"express": "^4.20.0", "express": "^4.19.2",
"install": "^0.13.0", "install": "^0.13.0",
"jest": "^29.6.3", "jest": "^29.6.3",
"nodemon": "^3.0.1", "nodemon": "^3.0.1",

View File

@@ -1,7 +1,6 @@
import React, { ReactElement } from "react"; import React, { ReactElement } from "react";
import { Outlet } from "react-router"; import { Outlet } from "react-router-dom";
import { Navbar2 } from "./shared/Navbar2"; import { Navbar2 } from "./shared/Navbar2";
import { ToastContainer } from "react-toastify";
import "../assets/scss/App.scss"; import "../assets/scss/App.scss";
export const App = (): ReactElement => { export const App = (): ReactElement => {
@@ -9,7 +8,6 @@ export const App = (): ReactElement => {
<> <>
<Navbar2 /> <Navbar2 />
<Outlet /> <Outlet />
<ToastContainer stacked hideProgressBar />
</> </>
); );
}; };

View File

@@ -1,4 +1,5 @@
import React, { useCallback, ReactElement, useEffect, useState } from "react"; import React, { useCallback, ReactElement, useEffect, useState } from "react";
import { getBundlesForComic, sleep } from "../../actions/airdcpp.actions";
import { SearchQuery, PriorityEnum, SearchResponse } from "threetwo-ui-typings"; import { SearchQuery, PriorityEnum, SearchResponse } from "threetwo-ui-typings";
import { RootState, SearchInstance } from "threetwo-ui-typings"; import { RootState, SearchInstance } from "threetwo-ui-typings";
import ellipsize from "ellipsize"; import ellipsize from "ellipsize";
@@ -33,20 +34,18 @@ export const AcquisitionPanel = (
priority: PriorityEnum; priority: PriorityEnum;
} }
interface SearchResult { interface SearchResult {
id: string; result: {
id: number;
};
search_id: number;
// Add other properties as needed // Add other properties as needed
slots: any;
type: any;
users: any;
name: string;
dupe: Boolean;
size: number;
} }
const handleSearch = (searchQuery) => { const handleSearch = (searchQuery) => {
// Use the already connected socket instance to emit events // Use the already connected socket instance to emit events
socketIOInstance.emit("initiateSearch", searchQuery); socketIOInstance.emit("initiateSearch", searchQuery);
}; };
const { const {
data: settings, data: settings,
isLoading, isLoading,
@@ -110,36 +109,42 @@ export const AcquisitionPanel = (
*/ */
const search = async (searchData: any) => { const search = async (searchData: any) => {
setAirDCPPSearchResults([]); setAirDCPPSearchResults([]);
socketIOInstance.emit("call", "socket.search", { socketIOInstance.emit(
query: searchData, "call",
config: { "socket.search",
protocol: `ws`, {
// hostname: `192.168.1.119:5600`, query: searchData,
hostname: `127.0.0.1:5600`, config: {
username: `user`, protocol: `ws`,
password: `pass`, hostname: `localhost:5600`,
username: `user`,
password: `pass`,
},
}, },
}); (data: any) => console.log(data),
);
}; };
socketIOInstance.on("searchResultAdded", ({ result }: any) => { socketIOInstance.on("searchResultAdded", (data: SearchResult) => {
setAirDCPPSearchResults((previousState) => { setAirDCPPSearchResults((previousState) => {
const exists = previousState.some((item) => result.id === item.id); const exists = previousState.some(
(item) => data.result.id === item.result.id,
);
if (!exists) { if (!exists) {
return [...previousState, result]; return [...previousState, data];
} }
return previousState; return previousState;
}); });
}); });
socketIOInstance.on("searchResultUpdated", ({ result }: any) => { socketIOInstance.on("searchResultUpdated", (groupedResult: SearchResult) => {
// ...update properties of the existing result in the UI // ...update properties of the existing result in the UI
const bundleToUpdateIndex = airDCPPSearchResults?.findIndex( const bundleToUpdateIndex = airDCPPSearchResults?.findIndex(
(bundle) => bundle.id === result.id, (bundle) => bundle.result.id === groupedResult.result.id,
); );
const updatedState = [...airDCPPSearchResults]; const updatedState = [...airDCPPSearchResults];
if (!isNil(difference(updatedState[bundleToUpdateIndex], result))) { if (!isNil(difference(updatedState[bundleToUpdateIndex], groupedResult))) {
updatedState[bundleToUpdateIndex] = result; updatedState[bundleToUpdateIndex] = groupedResult;
} }
setAirDCPPSearchResults((state) => [...state, ...updatedState]); setAirDCPPSearchResults((state) => [...state, ...updatedState]);
}); });
@@ -170,7 +175,7 @@ export const AcquisitionPanel = (
size: Number, size: Number,
type: any, type: any,
config: any, config: any,
): Promise<void> => { ): void => {
socketIOInstance.emit( socketIOInstance.emit(
"call", "call",
"socket.download", "socket.download",
@@ -192,7 +197,7 @@ export const AcquisitionPanel = (
pattern: `${searchQuery.issueName}`, pattern: `${searchQuery.issueName}`,
extensions: ["cbz", "cbr", "cb7"], extensions: ["cbz", "cbr", "cb7"],
}, },
hub_urls: [hubs?.data[0].hub_url], hub_urls: map(hubs?.data, (hub) => hub.hub_url),
priority: 5, priority: 5,
}; };
@@ -201,7 +206,7 @@ export const AcquisitionPanel = (
return ( return (
<> <>
<div className="mt-5 mb-3"> <div className="mt-5">
{!isEmpty(hubs?.data) ? ( {!isEmpty(hubs?.data) ? (
<Form <Form
onSubmit={getDCPPSearchResults} onSubmit={getDCPPSearchResults}
@@ -247,24 +252,16 @@ export const AcquisitionPanel = (
)} )}
/> />
) : ( ) : (
<article <div className="">
role="alert" <article className="">
className="mt-4 rounded-lg text-sm max-w-screen-md border-s-4 border-yellow-500 bg-yellow-50 p-4 dark:border-s-4 dark:border-yellow-600 dark:bg-yellow-300 dark:text-slate-600" <div className="">
> AirDC++ is not configured. Please configure it in{" "}
No AirDC++ hub configured. Please configure it in{" "} <code>Settings &gt; AirDC++ &gt; Connection</code>.
<code>Settings &gt; AirDC++ &gt; Hubs</code>. </div>
</article> </article>
</div>
)} )}
</div> </div>
{/* configured hub */}
{!isEmpty(hubs?.data) && (
<span className="inline-flex items-center bg-green-50 text-slate-800 text-xs font-medium px-2.5 py-0.5 rounded-md dark:text-slate-900 dark:bg-green-300">
<span className="pr-1 pt-1">
<i className="icon-[solar--server-2-bold-duotone] w-5 h-5"></i>
</span>
{hubs && hubs?.data[0].hub_url}
</span>
)}
{/* AirDC++ search instance details */} {/* AirDC++ search instance details */}
{!isNil(airDCPPSearchInstance) && {!isNil(airDCPPSearchInstance) &&
@@ -275,7 +272,7 @@ export const AcquisitionPanel = (
<dl> <dl>
<dt> <dt>
<div className="mb-1"> <div className="mb-1">
{hubs?.data.map((value, idx: string) => ( {hubs?.data.map((value, idx) => (
<span className="tag is-warning" key={idx}> <span className="tag is-warning" key={idx}>
{value.identity.name} {value.identity.name}
</span> </span>
@@ -314,7 +311,7 @@ export const AcquisitionPanel = (
)} )}
{/* AirDC++ results */} {/* AirDC++ results */}
<div className=""> <div className="columns">
{!isNil(airDCPPSearchResults) && !isEmpty(airDCPPSearchResults) ? ( {!isNil(airDCPPSearchResults) && !isEmpty(airDCPPSearchResults) ? (
<div className="overflow-x-auto w-fit mt-4 rounded-lg border border-gray-200 dark:border-gray-500"> <div className="overflow-x-auto w-fit mt-4 rounded-lg border border-gray-200 dark:border-gray-500">
<table className="min-w-full divide-y-2 divide-gray-200 dark:divide-gray-500 text-md"> <table className="min-w-full divide-y-2 divide-gray-200 dark:divide-gray-500 text-md">
@@ -335,121 +332,118 @@ export const AcquisitionPanel = (
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-slate-100 dark:divide-gray-500"> <tbody className="divide-y divide-slate-100 dark:divide-gray-500">
{map( {map(airDCPPSearchResults, ({ result, search_id }, idx) => {
airDCPPSearchResults, return (
({ dupe, type, name, id, slots, users, size }, idx) => { <tr
return ( key={idx}
<tr className={
key={idx} !isNil(result.dupe)
className={ ? "bg-gray-100 dark:bg-gray-700"
!isNil(dupe) : "w-fit text-sm"
? "bg-gray-100 dark:bg-gray-700" }
: "w-fit text-sm" >
} <td className="whitespace-nowrap px-3 py-3 text-gray-700 dark:text-slate-300">
> <p className="mb-2">
<td className="whitespace-nowrap px-3 py-3 text-gray-700 dark:text-slate-300"> {result.type.id === "directory" ? (
<p className="mb-2"> <i className="fas fa-folder"></i>
{type.id === "directory" ? ( ) : null}
<i className="fas fa-folder"></i> {ellipsize(result.name, 70)}
) : null} </p>
{ellipsize(name, 70)}
</p>
<dl> <dl>
<dd> <dd>
<div className="inline-flex flex-row gap-2"> <div className="inline-flex flex-row gap-2">
{!isNil(dupe) ? ( {!isNil(result.dupe) ? (
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
<span className="pr-1 pt-1">
<i className="icon-[solar--copy-bold-duotone] w-5 h-5"></i>
</span>
<span className="text-md text-slate-500 dark:text-slate-900">
Dupe
</span>
</span>
) : null}
{/* Nicks */}
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400"> <span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
<span className="pr-1 pt-1"> <span className="pr-1 pt-1">
<i className="icon-[solar--user-rounded-bold-duotone] w-5 h-5"></i> <i className="icon-[solar--copy-bold-duotone] w-5 h-5"></i>
</span> </span>
<span className="text-md text-slate-500 dark:text-slate-900"> <span className="text-md text-slate-500 dark:text-slate-900">
{users.user.nicks} Dupe
</span> </span>
</span> </span>
{/* Flags */} ) : null}
{users.user.flags.map((flag, idx) => (
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
<span className="pr-1 pt-1">
<i className="icon-[solar--tag-horizontal-bold-duotone] w-5 h-5"></i>
</span>
<span className="text-md text-slate-500 dark:text-slate-900"> {/* Nicks */}
{flag} <span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
</span> <span className="pr-1 pt-1">
<i className="icon-[solar--user-rounded-bold-duotone] w-5 h-5"></i>
</span>
<span className="text-md text-slate-500 dark:text-slate-900">
{result.users.user.nicks}
</span>
</span>
{/* Flags */}
{result.users.user.flags.map((flag, idx) => (
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
<span className="pr-1 pt-1">
<i className="icon-[solar--tag-horizontal-bold-duotone] w-5 h-5"></i>
</span> </span>
))}
</div>
</dd>
</dl>
</td>
<td>
{/* Extension */}
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
<span className="pr-1 pt-1">
<i className="icon-[solar--zip-file-bold-duotone] w-5 h-5"></i>
</span>
<span className="text-md text-slate-500 dark:text-slate-900"> <span className="text-md text-slate-500 dark:text-slate-900">
{type.str} {flag}
</span> </span>
</span>
))}
</div>
</dd>
</dl>
</td>
<td>
{/* Extension */}
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
<span className="pr-1 pt-1">
<i className="icon-[solar--zip-file-bold-duotone] w-5 h-5"></i>
</span> </span>
</td>
<td className="px-2">
{/* Slots */}
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
<span className="pr-1 pt-1">
<i className="icon-[solar--settings-minimalistic-bold-duotone] w-5 h-5"></i>
</span>
<span className="text-md text-slate-500 dark:text-slate-900"> <span className="text-md text-slate-500 dark:text-slate-900">
{slots.total} slots; {slots.free} free {result.type.str}
</span>
</span> </span>
</td> </span>
<td className="px-2"> </td>
<button <td className="px-2">
className="flex space-x-1 sm:mt-0 sm:flex-row sm:items-center rounded-lg border border-green-400 dark:border-green-200 bg-green-200 px-3 py-1 text-gray-500 hover:bg-transparent hover:text-green-600 focus:outline-none focus:ring active:text-indigo-500" {/* Slots */}
onClick={() => <span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
download( <span className="pr-1 pt-1">
airDCPPSearchInstance.id, <i className="icon-[solar--settings-minimalistic-bold-duotone] w-5 h-5"></i>
id, </span>
comicObjectId,
name, <span className="text-md text-slate-500 dark:text-slate-900">
size, {result.slots.total} slots; {result.slots.free} free
type, </span>
{ </span>
protocol: `ws`, </td>
hostname: `192.168.1.119:5600`, <td className="px-2">
username: `admin`, <button
password: `password`, className="flex space-x-1 sm:mt-0 sm:flex-row sm:items-center rounded-lg border border-green-400 dark:border-green-200 bg-green-200 px-3 py-1 text-gray-500 hover:bg-transparent hover:text-green-600 focus:outline-none focus:ring active:text-indigo-500"
}, onClick={() =>
) download(
} airDCPPSearchInstance.id,
> result.id,
<span className="text-xs">Download</span> comicObjectId,
<span className="w-5 h-5"> result.name,
<i className="h-5 w-5 icon-[solar--download-bold-duotone]"></i> result.size,
</span> result.type,
</button> {
</td> protocol: `ws`,
</tr> hostname: `localhost:5600`,
); username: `user`,
}, password: `pass`,
)} },
)
}
>
<span className="text-xs">Download</span>
<span className="w-5 h-5">
<i className="h-5 w-5 icon-[solar--download-bold-duotone]"></i>
</span>
</button>
</td>
</tr>
);
})}
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@@ -1,17 +1,11 @@
import React, { ReactElement, useCallback, useState } from "react"; import React, { ReactElement, useCallback, useState } from "react";
import PropTypes from "prop-types";
import { fetchMetronResource } from "../../../actions/metron.actions"; import { fetchMetronResource } from "../../../actions/metron.actions";
import Creatable from "react-select/creatable"; import Creatable from "react-select/creatable";
import { withAsyncPaginate } from "react-select-async-paginate"; import { withAsyncPaginate } from "react-select-async-paginate";
const CreatableAsyncPaginate = withAsyncPaginate(Creatable); const CreatableAsyncPaginate = withAsyncPaginate(Creatable);
interface AsyncSelectPaginateProps { export const AsyncSelectPaginate = (props): ReactElement => {
metronResource: string;
placeholder?: string;
value?: object;
onChange?(...args: unknown[]): unknown;
}
export const AsyncSelectPaginate = (props: AsyncSelectPaginateProps): ReactElement => {
const [value, setValue] = useState(null); const [value, setValue] = useState(null);
const [isAddingInProgress, setIsAddingInProgress] = useState(false); const [isAddingInProgress, setIsAddingInProgress] = useState(false);
@@ -44,4 +38,11 @@ export const AsyncSelectPaginate = (props: AsyncSelectPaginateProps): ReactEleme
); );
}; };
AsyncSelectPaginate.propTypes = {
metronResource: PropTypes.string.isRequired,
placeholder: PropTypes.string,
value: PropTypes.object,
onChange: PropTypes.func,
};
export default AsyncSelectPaginate; export default AsyncSelectPaginate;

View File

@@ -1,5 +1,5 @@
import React, { useState, ReactElement, useCallback } from "react"; import React, { useState, ReactElement, useCallback } from "react";
import { useParams } from "react-router"; import { useParams } from "react-router-dom";
import Card from "../shared/Carda"; import Card from "../shared/Carda";
import { ComicVineMatchPanel } from "./ComicVineMatchPanel"; import { ComicVineMatchPanel } from "./ComicVineMatchPanel";

View File

@@ -1,5 +1,5 @@
import React, { ReactElement } from "react"; import React, { ReactElement } from "react";
import { useParams } from "react-router"; import { useParams } from "react-router-dom";
import { ComicDetail } from "../ComicDetail/ComicDetail"; import { ComicDetail } from "../ComicDetail/ComicDetail";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { LIBRARY_SERVICE_BASE_URI } from "../../constants/endpoints"; import { LIBRARY_SERVICE_BASE_URI } from "../../constants/endpoints";

View File

@@ -1,21 +1,12 @@
import React, { ReactElement } from "react"; import React, { ReactElement } from "react";
import PropTypes from "prop-types";
import { detectIssueTypes } from "../../shared/utils/tradepaperback.utils"; import { detectIssueTypes } from "../../shared/utils/tradepaperback.utils";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { isEmpty, isUndefined } from "lodash"; import { isEmpty, isUndefined } from "lodash";
import Card from "../shared/Carda"; import Card from "../shared/Carda";
import { convert } from "html-to-text"; import { convert } from "html-to-text";
interface ComicVineDetailsProps { export const ComicVineDetails = (props): ReactElement => {
updatedAt?: string;
data?: {
name?: string;
number?: string;
resource_type?: string;
id?: number;
};
}
export const ComicVineDetails = (props: ComicVineDetailsProps): ReactElement => {
const { data, updatedAt } = props; const { data, updatedAt } = props;
return ( return (
<div className="text-slate-500 dark:text-gray-400"> <div className="text-slate-500 dark:text-gray-400">
@@ -116,3 +107,13 @@ export const ComicVineDetails = (props: ComicVineDetailsProps): ReactElement =>
}; };
export default ComicVineDetails; export default ComicVineDetails;
ComicVineDetails.propTypes = {
updatedAt: PropTypes.string,
data: PropTypes.shape({
name: PropTypes.string,
number: PropTypes.string,
resource_type: PropTypes.string,
id: PropTypes.number,
}),
};

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useContext, ReactElement, useState } from "react"; import React, { useEffect, useContext, ReactElement, useState } from "react";
import { RootState } from "threetwo-ui-typings"; import { RootState } from "threetwo-ui-typings";
import { isEmpty, isNil, isUndefined, map } from "lodash"; import { isEmpty, map } from "lodash";
import { AirDCPPBundles } from "./AirDCPPBundles"; import { AirDCPPBundles } from "./AirDCPPBundles";
import { TorrentDownloads } from "./TorrentDownloads"; import { TorrentDownloads } from "./TorrentDownloads";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
@@ -9,11 +9,10 @@ import {
LIBRARY_SERVICE_BASE_URI, LIBRARY_SERVICE_BASE_URI,
QBITTORRENT_SERVICE_BASE_URI, QBITTORRENT_SERVICE_BASE_URI,
TORRENT_JOB_SERVICE_BASE_URI, TORRENT_JOB_SERVICE_BASE_URI,
SOCKET_BASE_URI,
} from "../../constants/endpoints"; } from "../../constants/endpoints";
import { useStore } from "../../store"; import { useStore } from "../../store";
import { useShallow } from "zustand/react/shallow"; import { useShallow } from "zustand/react/shallow";
import { useParams } from "react-router"; import { useParams } from "react-router-dom";
interface IDownloadsPanelProps { interface IDownloadsPanelProps {
key: number; key: number;
@@ -23,11 +22,13 @@ export const DownloadsPanel = (
props: IDownloadsPanelProps, props: IDownloadsPanelProps,
): ReactElement | null => { ): ReactElement | null => {
const { comicObjectId } = useParams<{ comicObjectId: string }>(); const { comicObjectId } = useParams<{ comicObjectId: string }>();
const [bundles, setBundles] = useState([]);
const [infoHashes, setInfoHashes] = useState<string[]>([]); const [infoHashes, setInfoHashes] = useState<string[]>([]);
const [torrentDetails, setTorrentDetails] = useState([]); const [torrentDetails, setTorrentDetails] = useState([]);
const [activeTab, setActiveTab] = useState("directconnect"); const [activeTab, setActiveTab] = useState("torrents");
const { socketIOInstance } = useStore( const { airDCPPSocketInstance, socketIOInstance } = useStore(
useShallow((state: any) => ({ useShallow((state: any) => ({
airDCPPSocketInstance: state.airDCPPSocketInstance,
socketIOInstance: state.socketIOInstance, socketIOInstance: state.socketIOInstance,
})), })),
); );
@@ -43,29 +44,32 @@ export const DownloadsPanel = (
.filter((item) => item !== undefined); .filter((item) => item !== undefined);
setTorrentDetails(torrents); setTorrentDetails(torrents);
}); });
// Fetch the downloaded files and currently-downloading file(s) from AirDC++
/** const { data: comicObject, isSuccess } = useQuery({
* Query to fetch AirDC++ download bundles for a given comic resource Id
* @param {string} {comicObjectId} - A mongo id that identifies a comic document
*/
const { data: bundles } = useQuery({
queryKey: ["bundles"], queryKey: ["bundles"],
queryFn: async () => queryFn: async () =>
await axios({ await axios({
url: `${LIBRARY_SERVICE_BASE_URI}/getBundles`, url: `${LIBRARY_SERVICE_BASE_URI}/getComicBookById`,
method: "POST", method: "POST",
headers: {
"Content-Type": "application/json; charset=utf-8",
},
data: { data: {
comicObjectId, id: `${comicObjectId}`,
config: {
protocol: `ws`,
hostname: `192.168.1.119:5600`,
username: `admin`,
password: `password`,
},
}, },
}), }),
enabled: activeTab !== "" && activeTab === "directconnect",
}); });
const getBundles = async (comicObject) => {
if (comicObject?.data.acquisition.directconnect) {
const filteredBundles =
comicObject.data.acquisition.directconnect.downloads.map(
async ({ bundleId }) => {
return await airDCPPSocketInstance.get(`queue/bundles/${bundleId}`);
},
);
return await Promise.all(filteredBundles);
}
};
// Call the scheduled job for fetching torrent data // Call the scheduled job for fetching torrent data
// triggered by the active tab been set to "torrents" // triggered by the active tab been set to "torrents"
@@ -79,9 +83,14 @@ export const DownloadsPanel = (
}, },
}), }),
queryKey: [activeTab], queryKey: [activeTab],
enabled: activeTab !== "" && activeTab === "torrents",
}); });
console.log(bundles);
useEffect(() => {
getBundles(comicObject).then((result) => {
setBundles(result);
});
}, [comicObject]);
return ( return (
<div className="columns is-multiline"> <div className="columns is-multiline">
<div> <div>
@@ -126,14 +135,10 @@ export const DownloadsPanel = (
</div> </div>
</div> </div>
{activeTab === "torrents" ? ( {activeTab === "torrents" && <TorrentDownloads data={torrentDetails} />}
<TorrentDownloads data={torrentDetails} /> {!isEmpty(airDCPPSocketInstance) &&
) : null} !isEmpty(bundles) &&
{!isNil(bundles?.data) && bundles?.data.length !== 0 ? ( activeTab === "directconnect" && <AirDCPPBundles data={bundles} />}
<AirDCPPBundles data={bundles.data} />
) : (
"nutin"
)}
</div> </div>
); );
}; };

View File

@@ -1,36 +1,10 @@
import React, { ReactElement } from "react"; import React, { ReactElement } from "react";
import PropTypes from "prop-types";
import prettyBytes from "pretty-bytes"; import prettyBytes from "pretty-bytes";
import { isEmpty } from "lodash"; import { isEmpty } from "lodash";
import { format, parseISO } from "date-fns"; import { format, parseISO } from "date-fns";
interface RawFileDetailsProps { export const RawFileDetails = (props): ReactElement => {
data?: {
rawFileDetails?: {
containedIn?: string;
name?: string;
fileSize?: number;
path?: string;
extension?: string;
mimeType?: string;
cover?: {
filePath?: string;
};
};
inferredMetadata?: {
issue?: {
year?: string;
name?: string;
number?: number;
subtitle?: string;
};
};
created_at?: string;
updated_at?: string;
};
children?: any;
}
export const RawFileDetails = (props: RawFileDetailsProps): ReactElement => {
const { rawFileDetails, inferredMetadata, created_at, updated_at } = const { rawFileDetails, inferredMetadata, created_at, updated_at } =
props.data; props.data;
return ( return (
@@ -124,3 +98,30 @@ export const RawFileDetails = (props: RawFileDetailsProps): ReactElement => {
}; };
export default RawFileDetails; export default RawFileDetails;
RawFileDetails.propTypes = {
data: PropTypes.shape({
rawFileDetails: PropTypes.shape({
containedIn: PropTypes.string,
name: PropTypes.string,
fileSize: PropTypes.number,
path: PropTypes.string,
extension: PropTypes.string,
mimeType: PropTypes.string,
cover: PropTypes.shape({
filePath: PropTypes.string,
}),
}),
inferredMetadata: PropTypes.shape({
issue: PropTypes.shape({
year: PropTypes.string,
name: PropTypes.string,
number: PropTypes.number,
subtitle: PropTypes.string,
}),
}),
created_at: PropTypes.string,
updated_at: PropTypes.string,
}),
children: PropTypes.any,
};

View File

@@ -25,7 +25,7 @@ export const TorrentSearchPanel = (props) => {
data: { data: {
prowlarrQuery: { prowlarrQuery: {
port: "9696", port: "9696",
apiKey: "38c2656e8f5d4790962037b8c4416a8f", apiKey: "c4f42e265fb044dc81f7e88bd41c3367",
offset: 0, offset: 0,
categories: [7030], categories: [7030],
query: searchTerm.issueName, query: searchTerm.issueName,

View File

@@ -4,7 +4,7 @@ import Card from "../shared/Carda";
import Header from "../shared/Header"; import Header from "../shared/Header";
import { importToDB } from "../../actions/fileops.actions"; import { importToDB } from "../../actions/fileops.actions";
import ellipsize from "ellipsize"; import ellipsize from "ellipsize";
import { Link } from "react-router"; import { Link } from "react-router-dom";
import axios from "axios"; import axios from "axios";
import rateLimiter from "axios-rate-limit"; import rateLimiter from "axios-rate-limit";
@@ -86,8 +86,8 @@ export const PullList = (): ReactElement => {
<span className="text-md"> <span className="text-md">
Pull List aggregated for the week from{" "} Pull List aggregated for the week from{" "}
<span className="underline"> <span className="underline">
<a href="https://www.tfaw.com/comics/new-releases.html"> <a href="https://leagueofcomicgeeks.com/comics/new-comics">
Things From Another World League Of Comic Geeks
</a> </a>
<i className="icon-[solar--arrow-right-up-outline] w-4 h-4" /> <i className="icon-[solar--arrow-right-up-outline] w-4 h-4" />
</span> </span>
@@ -132,13 +132,13 @@ export const PullList = (): ReactElement => {
<div key={idx} className="keen-slider__slide"> <div key={idx} className="keen-slider__slide">
<Card <Card
orientation={"vertical-2"} orientation={"vertical-2"}
imageUrl={issue.coverImageUrl} imageUrl={issue.cover}
hasDetails hasDetails
title={ellipsize(issue.name, 25)} title={ellipsize(issue.name, 25)}
> >
<div className="px-1"> <div className="px-1">
<span className="inline-flex mb-2 items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-1 rounded-md dark:text-slate-900 dark:bg-slate-400"> <span className="inline-flex mb-2 items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-1 rounded-md dark:text-slate-900 dark:bg-slate-400">
{issue.publicationDate} {issue.publisher}
</span> </span>
<div className="flex flex-row justify-end"> <div className="flex flex-row justify-end">
<button <button

View File

@@ -1,6 +1,6 @@
import React, { ReactElement } from "react"; import React, { ReactElement } from "react";
import Card from "../shared/Carda"; import Card from "../shared/Carda";
import { Link } from "react-router"; import { Link } from "react-router-dom";
import ellipsize from "ellipsize"; import ellipsize from "ellipsize";
import { isEmpty, isNil, isUndefined, map } from "lodash"; import { isEmpty, isNil, isUndefined, map } from "lodash";
import { detectIssueTypes } from "../../shared/utils/tradepaperback.utils"; import { detectIssueTypes } from "../../shared/utils/tradepaperback.utils";

View File

@@ -1,7 +1,7 @@
import { map, unionBy } from "lodash"; import { map, unionBy } from "lodash";
import React, { ReactElement } from "react"; import React, { ReactElement } from "react";
import ellipsize from "ellipsize"; import ellipsize from "ellipsize";
import { Link, useNavigate } from "react-router"; import { Link, useNavigate } from "react-router-dom";
import Card from "../shared/Carda"; import Card from "../shared/Carda";
import Header from "../shared/Header"; import Header from "../shared/Header";

View File

@@ -1,6 +1,6 @@
import React, { ReactElement } from "react"; import React, { ReactElement } from "react";
import Card from "../shared/Carda"; import Card from "../shared/Carda";
import { Link, useNavigate } from "react-router"; import { Link, useNavigate } from "react-router-dom";
import ellipsize from "ellipsize"; import ellipsize from "ellipsize";
import { isEmpty, isNil, isUndefined, map } from "lodash"; import { isEmpty, isNil, isUndefined, map } from "lodash";
import { detectIssueTypes } from "../../shared/utils/tradepaperback.utils"; import { detectIssueTypes } from "../../shared/utils/tradepaperback.utils";

View File

@@ -1,6 +1,6 @@
import React, { useMemo, ReactElement, useState, useEffect } from "react"; import React, { useMemo, ReactElement, useState, useEffect } from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router-dom";
import { isEmpty, isNil, isUndefined } from "lodash"; import { isEmpty, isNil, isUndefined } from "lodash";
import MetadataPanel from "../shared/MetadataPanel"; import MetadataPanel from "../shared/MetadataPanel";
import T2Table from "../shared/T2Table"; import T2Table from "../shared/T2Table";

View File

@@ -14,7 +14,7 @@ import { isNil, isEmpty, isUndefined } from "lodash";
import Masonry from "react-masonry-css"; import Masonry from "react-masonry-css";
import Card from "../shared/Carda"; import Card from "../shared/Carda";
import { detectIssueTypes } from "../../shared/utils/tradepaperback.utils"; import { detectIssueTypes } from "../../shared/utils/tradepaperback.utils";
import { Link } from "react-router"; import { Link } from "react-router-dom";
import { LIBRARY_SERVICE_HOST } from "../../constants/endpoints"; import { LIBRARY_SERVICE_HOST } from "../../constants/endpoints";
interface ILibraryGridProps {} interface ILibraryGridProps {}

View File

@@ -1,7 +1,7 @@
import React, { ReactElement } from "react"; import React, { ReactElement } from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { Form, Field } from "react-final-form"; import { Form, Field } from "react-final-form";
import { Link } from "react-router"; import { Link } from "react-router-dom";
export const SearchBar = (props): ReactElement => { export const SearchBar = (props): ReactElement => {
const { searchHandler } = props; const { searchHandler } = props;

View File

@@ -63,6 +63,7 @@ export const Search = ({}: ISearchProps): ReactElement => {
markEntireVolumeWanted, markEntireVolumeWanted,
resourceType, resourceType,
}) => { }) => {
console.log("jigni", comicObject);
let volumeInformation = {}; let volumeInformation = {};
let issues = []; let issues = [];
switch (resourceType) { switch (resourceType) {
@@ -72,12 +73,11 @@ export const Search = ({}: ISearchProps): ReactElement => {
// Add issue metadata // Add issue metadata
issues.push({ issues.push({
id, id,
url: api_detail_url, api_detail_url,
image, image,
coverDate: cover_date, coverDate: cover_date,
issueNumber: issue_number, issueNumber: issue_number,
}); });
console.log(issues);
// Get volume metadata from CV // Get volume metadata from CV
const response = await axios({ const response = await axios({
url: `${COMICVINE_SERVICE_URI}/getVolumes`, url: `${COMICVINE_SERVICE_URI}/getVolumes`,

View File

@@ -1,10 +1,9 @@
import React, { ReactElement, useState } from "react"; import React, { ReactElement, useEffect, useState, useContext } from "react";
import { Form, Field } from "react-final-form"; import { Form, Field } from "react-final-form";
import { isEmpty, isNil, isUndefined } from "lodash"; import { isEmpty, isNil, isUndefined } from "lodash";
import Select from "react-select"; import Select from "react-select";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import axios from "axios"; import axios from "axios";
import { produce } from "immer";
import { AIRDCPP_SERVICE_BASE_URI } from "../../../constants/endpoints"; import { AIRDCPP_SERVICE_BASE_URI } from "../../../constants/endpoints";
export const AirDCPPHubsForm = (): ReactElement => { export const AirDCPPHubsForm = (): ReactElement => {
@@ -14,7 +13,6 @@ export const AirDCPPHubsForm = (): ReactElement => {
data: settings, data: settings,
isLoading, isLoading,
isError, isError,
refetch,
} = useQuery({ } = useQuery({
queryKey: ["settings"], queryKey: ["settings"],
queryFn: async () => queryFn: async () =>
@@ -22,9 +20,11 @@ export const AirDCPPHubsForm = (): ReactElement => {
url: "http://localhost:3000/api/settings/getAllSettings", url: "http://localhost:3000/api/settings/getAllSettings",
method: "GET", method: "GET",
}), }),
staleTime: Infinity,
}); });
/**
* Get the hubs list from an AirDCPP Socket
*/
const { data: hubs } = useQuery({ const { data: hubs } = useQuery({
queryKey: ["hubs"], queryKey: ["hubs"],
queryFn: async () => queryFn: async () =>
@@ -37,16 +37,14 @@ export const AirDCPPHubsForm = (): ReactElement => {
}), }),
enabled: !isEmpty(settings?.data.directConnect?.client?.host), enabled: !isEmpty(settings?.data.directConnect?.client?.host),
}); });
let hubList = {};
let hubList: any[] = [];
if (!isNil(hubs)) { if (!isNil(hubs)) {
hubList = hubs?.data.map(({ hub_url, identity }) => ({ hubList = hubs?.data.map(({ hub_url, identity }) => ({
value: hub_url, value: hub_url,
label: identity.name, label: identity.name,
})); }));
} }
const { mutate } = useMutation({
const mutation = useMutation({
mutationFn: async (values) => mutationFn: async (values) =>
await axios({ await axios({
url: `http://localhost:3000/api/settings/saveSettings`, url: `http://localhost:3000/api/settings/saveSettings`,
@@ -57,112 +55,82 @@ export const AirDCPPHubsForm = (): ReactElement => {
settingsKey: "directConnect", settingsKey: "directConnect",
}, },
}), }),
onSuccess: (data) => { onSuccess: () => {
queryClient.setQueryData(["settings"], (oldData: any) => queryClient.invalidateQueries({ queryKey: ["settings"] });
produce(oldData, (draft: any) => {
draft.data.directConnect.client = {
...draft.data.directConnect.client,
...data.data.directConnect.client,
};
}),
);
}, },
}); });
const validate = async () => {};
const validate = async (values) => {
const errors = {};
// Add any validation logic here if needed
return errors;
};
const SelectAdapter = ({ input, ...rest }) => { const SelectAdapter = ({ input, ...rest }) => {
return <Select {...input} {...rest} isClearable isMulti />; return <Select {...input} {...rest} isClearable isMulti />;
}; };
if (isLoading) {
return <div>Loading...</div>;
}
if (isError) {
return <div>Error loading settings.</div>;
}
return ( return (
<> <>
{!isEmpty(hubList) && !isUndefined(hubs) ? ( {!isEmpty(hubList) && !isUndefined(hubs) ? (
<Form <Form
onSubmit={(values) => { onSubmit={mutate}
mutation.mutate(values);
}}
validate={validate} validate={validate}
render={({ handleSubmit }) => ( render={({ handleSubmit }) => (
<form onSubmit={handleSubmit} className="mt-10"> <form onSubmit={handleSubmit}>
<h2 className="text-xl">Configure DC++ Hubs</h2> <div>
<article <h3 className="title">Hubs</h3>
role="alert"
className="mt-4 rounded-lg max-w-screen-md border-s-4 border-blue-500 bg-blue-50 p-4 dark:border-s-4 dark:border-blue-600 dark:bg-blue-300 dark:text-slate-600"
>
<h6 className="subtitle has-text-grey-light"> <h6 className="subtitle has-text-grey-light">
Select the hubs you want to perform searches against. Your Select the hubs you want to perform searches against.
selection in the dropdown <strong>will replace</strong> the
existing selection.
</h6> </h6>
</article>
<div className="field">
<label className="block py-1 mt-3">AirDC++ Host</label>
<Field
name="hubs"
component={SelectAdapter}
className="basic-multi-select"
placeholder="Select Hubs to Search Against"
options={hubList}
/>
</div> </div>
<button <div className="field">
type="submit" <label className="label">AirDC++ Host</label>
className="flex space-x-1 sm:mt-5 sm:flex-row sm:items-center rounded-lg border border-green-400 dark:border-green-200 bg-green-200 px-4 py-2 text-gray-500 hover:bg-transparent hover:text-green-600 focus:outline-none focus:ring active:text-indigo-500" <div className="control">
> <Field
name="hubs"
component={SelectAdapter}
className="basic-multi-select"
placeholder="Select Hubs to Search Against"
options={hubList}
/>
</div>
</div>
<button type="submit" className="button is-primary">
Submit Submit
</button> </button>
</form> </form>
)} )}
/> />
) : ( ) : (
<article <>
role="alert" <article
className="mt-4 rounded-lg max-w-screen-md border-s-4 border-yellow-500 bg-yellow-50 p-4 dark:border-s-4 dark:border-yellow-600 dark:bg-yellow-300 dark:text-slate-600" role="alert"
> className="mt-4 rounded-lg max-w-screen-md border-s-4 border-yellow-500 bg-yellow-50 p-4 dark:border-s-4 dark:border-yellow-600 dark:bg-yellow-300 dark:text-slate-600"
<div className="message-body"> >
No configured hubs detected in AirDC++. <br /> <div className="message-body">
Configure to a hub in AirDC++ and then select a default hub here. No configured hubs detected in AirDC++. <br />
</div> Configure to a hub in AirDC++ and then select a default hub here.
</article> </div>
</article>
</>
)} )}
{!isEmpty(settings?.data.directConnect?.client.hubs) ? ( {!isEmpty(settings?.data.directConnect?.client.hubs) ? (
<> <>
<div className="mt-4"> <div className="mt-4">
<article className="message is-warning"> <article className="message is-warning">
<div className="message-body is-size-6 is-family-secondary"></div> <div className="message-body is-size-6 is-family-secondary">
Your selection in the dropdown <strong>will replace</strong> the
existing selection.
</div>
</article> </article>
</div> </div>
<div> <div className="box mt-3">
<span className="flex items-center mt-10 mb-4"> <h6>Default Hub For Searches:</h6>
<span className="text-xl text-slate-500 dark:text-slate-200 pr-5"> {settings?.data.directConnect?.client.hubs.map(
Default Hub for Searches ({ value, label }) => (
</span> <div key={value}>
<span className="h-px flex-1 bg-slate-200 dark:bg-slate-400"></span> <div>{label}</div>
</span> <span className="is-size-7">{value}</span>
<div className="block max-w-sm p-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-slate-400 dark:border-gray-700"> </div>
{settings?.data.directConnect?.client.hubs.map( ),
({ value, label }) => ( )}
<div key={value}>
<div>{label}</div>
<span className="is-size-7">{value}</span>
</div>
),
)}
</div>
</div> </div>
</> </>
) : null} ) : null}

View File

@@ -4,7 +4,7 @@ import Card from "../shared/Carda";
import T2Table from "../shared/T2Table"; import T2Table from "../shared/T2Table";
import ellipsize from "ellipsize"; import ellipsize from "ellipsize";
import { convert } from "html-to-text"; import { convert } from "html-to-text";
import { Link } from "react-router"; import { Link } from "react-router-dom";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import axios from "axios"; import axios from "axios";
import { SEARCH_SERVICE_BASE_URI } from "../../constants/endpoints"; import { SEARCH_SERVICE_BASE_URI } from "../../constants/endpoints";

View File

@@ -83,7 +83,7 @@ const renderCard = (props: ICardProps): ReactElement => {
case "vertical-2": case "vertical-2":
return ( return (
<div className="block rounded-md max-w-64 h-fit shadow-md shadow-white-400 bg-gray-200 dark:bg-slate-500"> <div className="block rounded-md w-64 h-fit shadow-md shadow-white-400 bg-gray-200 dark:bg-slate-500">
<img <img
alt="Home" alt="Home"
src={props.imageUrl} src={props.imageUrl}

View File

@@ -1,5 +1,5 @@
import React, { ReactElement } from "react"; import React, { ReactElement } from "react";
import { Link } from "react-router"; import { Link } from "react-router-dom";
type IHeaderProps = { type IHeaderProps = {
headerContent: string; headerContent: string;

View File

@@ -1,5 +1,5 @@
import React, { ReactElement, useState } from "react"; import React, { ReactElement, useState } from "react";
import { Link } from "react-router"; import { Link } from "react-router-dom";
import { useDarkMode } from "../../hooks/useDarkMode"; import { useDarkMode } from "../../hooks/useDarkMode";
export const Navbar2 = (): ReactElement => { export const Navbar2 = (): ReactElement => {

View File

@@ -1,4 +1,5 @@
import React, { ReactElement, useMemo, useState } from "react"; import React, { ReactElement, useMemo, useState } from "react";
import PropTypes from "prop-types";
import { import {
ColumnDef, ColumnDef,
flexRender, flexRender,
@@ -8,19 +9,7 @@ import {
PaginationState, PaginationState,
} from "@tanstack/react-table"; } from "@tanstack/react-table";
interface T2TableProps { export const T2Table = (tableOptions): ReactElement => {
sourceData?: unknown[];
totalPages?: number;
columns?: unknown[];
paginationHandlers?: {
nextPage?(...args: unknown[]): unknown;
previousPage?(...args: unknown[]): unknown;
};
rowClickHandler?(...args: unknown[]): unknown;
children?: any;
}
export const T2Table = (tableOptions: T2TableProps): ReactElement => {
const { const {
sourceData, sourceData,
columns, columns,
@@ -153,4 +142,15 @@ export const T2Table = (tableOptions: T2TableProps): ReactElement => {
); );
}; };
T2Table.propTypes = {
sourceData: PropTypes.array,
totalPages: PropTypes.number,
columns: PropTypes.array,
paginationHandlers: PropTypes.shape({
nextPage: PropTypes.func,
previousPage: PropTypes.func,
}),
rowClickHandler: PropTypes.func,
children: PropTypes.any,
};
export default T2Table; export default T2Table;

View File

@@ -1,8 +1,12 @@
import React from "react";
import { render } from "react-dom";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import App from "./components/App"; import App from "./components/App";
import { createBrowserRouter, RouterProvider } from "react-router"; import { createBrowserRouter, RouterProvider } from "react-router-dom";
import Settings from "./components/Settings/Settings"; import Settings from "./components/Settings/Settings";
import { ErrorPage } from "./components/shared/ErrorPage"; import { ErrorPage } from "./components/shared/ErrorPage";
const rootEl = document.getElementById("root");
const root = createRoot(rootEl);
import i18n from "./shared/utils/i18n.util"; import i18n from "./shared/utils/i18n.util";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import Import from "./components/Import/Import"; import Import from "./components/Import/Import";
@@ -16,48 +20,34 @@ import WantedComics from "./components/WantedComics/WantedComics";
const queryClient = new QueryClient(); const queryClient = new QueryClient();
const router = createBrowserRouter( const router = createBrowserRouter([
[
{
path: "/",
element: <App />,
errorElement: <ErrorPage />,
children: [
{ path: "/", element: <Dashboard /> },
{ path: "dashboard", element: <Dashboard /> },
{ path: "settings", element: <Settings /> },
{ path: "library", element: <TabulatedContentContainer category="library" /> },
{ path: "comic/details/:comicObjectId", element: <ComicDetailContainer /> },
{ path: "import", element: <Import path={"./comics"} /> },
{ path: "search", element: <Search /> },
{ path: "volume/details/:comicObjectId", element: <VolumeDetails /> },
{ path: "volumes", element: <Volumes /> },
{ path: "wanted", element: <WantedComics /> },
],
},
],
{ {
future: { path: "/",
v7_relativeSplatPath: true, element: <App />,
v7_fetcherPersist: true, errorElement: <ErrorPage />,
v7_normalizeFormMethod: true, children: [
v7_partialHydration: true, { path: "/", element: <Dashboard /> },
v7_skipActionErrorRevalidation: true, { path: "dashboard", element: <Dashboard /> },
}, { path: "settings", element: <Settings /> },
} {
); path: "library",
element: <TabulatedContentContainer category="library" />,
},
{
path: "comic/details/:comicObjectId",
element: <ComicDetailContainer />,
},
{ path: "import", element: <Import path={"./comics"} /> },
{ path: "search", element: <Search /> },
{ path: "volume/details/:comicObjectId", element: <VolumeDetails /> },
{ path: "volumes", element: <Volumes /> },
{ path: "wanted", element: <WantedComics /> },
],
},
]);
const rootElement = document.getElementById("root"); root.render(
if (rootElement) { <QueryClientProvider client={queryClient}>
const root = createRoot(rootElement); <RouterProvider router={router} />
root.render( </QueryClientProvider>,
<QueryClientProvider client={queryClient}> );
<RouterProvider
router={router}
future={{ v7_startTransition: true }}
/>
</QueryClientProvider>
);
} else {
console.error("Root element not found");
}

View File

@@ -45,8 +45,8 @@ export const determineCoverFile = (data): any => {
// comicvine // comicvine
if (!isEmpty(data.comicvine)) { if (!isEmpty(data.comicvine)) {
coverFile.comicvine.url = data?.comicvine?.image.small_url; coverFile.comicvine.url = data?.comicvine?.image.small_url;
coverFile.comicvine.issueName = data.comicvine?.name; coverFile.comicvine.issueName = data.comicvine.name;
coverFile.comicvine.publisher = data.comicvine?.publisher?.name; coverFile.comicvine.publisher = data.comicvine.publisher.name;
} }
// rawFileDetails // rawFileDetails
if (!isEmpty(data.rawFileDetails)) { if (!isEmpty(data.rawFileDetails)) {

View File

@@ -4,8 +4,6 @@ import io from "socket.io-client";
import { SOCKET_BASE_URI } from "../constants/endpoints"; import { SOCKET_BASE_URI } from "../constants/endpoints";
import { produce } from "immer"; import { produce } from "immer";
import { QueryClient } from "@tanstack/react-query"; import { QueryClient } from "@tanstack/react-query";
import { toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.min.css";
/* Broadly, this file sets up: /* Broadly, this file sets up:
* 1. The zustand-based global client state * 1. The zustand-based global client state
@@ -84,7 +82,6 @@ if (!isNil(sessionId)) {
"call", "call",
"socket.resumeSession", "socket.resumeSession",
{ {
namespace: "/",
sessionId, sessionId,
}, },
(data) => console.log(data), (data) => console.log(data),
@@ -132,11 +129,6 @@ socketIOInstance.on("LS_COVER_EXTRACTION_FAILED", (data) => {
})); }));
}); });
socketIOInstance.on("searchResultsAvailable", (data) => {
console.log(data);
toast(`Results found for query: ${JSON.stringify(data.query, null, 2)}`);
});
// 1b. Clear the localStorage sessionId upon receiving the // 1b. Clear the localStorage sessionId upon receiving the
// LS_IMPORT_QUEUE_DRAINED event // LS_IMPORT_QUEUE_DRAINED event
socketIOInstance.on("LS_IMPORT_QUEUE_DRAINED", (data) => { socketIOInstance.on("LS_IMPORT_QUEUE_DRAINED", (data) => {

6563
yarn.lock

File diff suppressed because it is too large Load Diff