Compare commits

...

25 Commits

Author SHA1 Message Date
540c711904 🖼️ Adding screenshots as of December 2022 2022-12-06 13:51:25 -08:00
0e73f9dad1 Merge branch 'master' of https://github.com/rishighan/threetwo 2022-11-24 22:00:25 -08:00
a15168a6be 📝 Added JSDoc to extractComicArchive method 2022-11-24 21:51:49 -08:00
dependabot[bot]
67079f0cb4 Bump engine.io from 6.2.0 to 6.2.1 (#42)
Bumps [engine.io](https://github.com/socketio/engine.io) from 6.2.0 to 6.2.1.
- [Release notes](https://github.com/socketio/engine.io/releases)
- [Changelog](https://github.com/socketio/engine.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/engine.io/compare/6.2.0...6.2.1)

---
updated-dependencies:
- dependency-name: engine.io
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-22 10:03:42 -08:00
ef5af01e33 ✏️ Code cleanup and adding jsDoc 2022-11-19 11:23:44 -08:00
5b6c9c8ffc ✏️ Added jsDoc for some components 2022-11-17 15:55:52 -08:00
4556778a47 🔧 Tweaking layout of the CV manual search form 2022-11-17 14:06:26 -08:00
af5f443cbe 🔧 Fixing CV manual search form 2022-11-17 13:46:20 -08:00
7babf9f73d 🔧 🏗️ Massive tables refactor
Abstracted a table component that can be configured to display issues, volumes or pull list items
2022-11-15 17:22:50 -08:00
1e39daeda2 🔧 Fixed wanted comics table data source 2022-11-08 10:05:30 -08:00
e3cea24615 🔧 Using the refactored T2Table for Wanted Comics 2022-11-07 21:15:02 -08:00
f60c9e4e67 🔧 Refactored the T2Table component with the new pagination controls 2022-11-07 20:55:58 -08:00
27e6f26331 ✏️ Changed the hard-coded date of the pull list 2022-11-07 08:06:09 -08:00
1f5502ce23 👁️ Added grid and tabular view buttons 2022-11-05 09:28:54 -07:00
3cb9588bbf 🎨 Styling tweaks to pagination controls 2022-11-03 21:40:23 -07:00
b1fb256189 🔧 Streamlined search and pagination controls on Library page 2022-11-03 12:47:31 -07:00
74ea2742f0 🔧 Fixes for controlled pagination on react-table 2022-11-01 22:21:58 -07:00
151c6ec314 🔎 Fixed search bar on Library page 2022-10-29 15:47:27 -07:00
c6a3be968a 🎨 Tweaking styling of the library table 2022-10-28 21:46:16 -07:00
ff5ce10e17 🔧 Library page table pagination 2022-10-27 23:06:40 -07:00
63e96bf96e 🚏 Added a path to comic detail row on Library page 2022-10-17 11:30:30 -07:00
b699a90a00 🕵🏼 Added manual search form for CV matching 2022-09-28 09:51:48 -07:00
eda11d3537 🐛 Fixed bug on the AirDC++ settings page 2022-09-19 15:41:32 -07:00
262f8d49d7 🔼 Bumped up react-final-form 2022-09-12 12:50:30 -07:00
6b1bb02d57 📝 Added a port number field to AirDC++ settings form 2022-09-08 10:35:30 -07:00
29 changed files with 613 additions and 360 deletions

BIN
ComicVine Matching.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

BIN
DC++ integration.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1013 KiB

BIN
Dashboard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

BIN
Library.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -69,7 +69,7 @@
"react-day-picker": "^8.0.6", "react-day-picker": "^8.0.6",
"react-dom": "^18.1.0", "react-dom": "^18.1.0",
"react-fast-compare": "^3.2.0", "react-fast-compare": "^3.2.0",
"react-final-form": "^6.5.3", "react-final-form": "^6.5.9",
"react-final-form-arrays": "^3.1.3", "react-final-form-arrays": "^3.1.3",
"react-loader-spinner": "^4.0.0", "react-loader-spinner": "^4.0.0",
"react-masonry-css": "^1.0.16", "react-masonry-css": "^1.0.16",

View File

@@ -32,6 +32,8 @@ import {
SS_SEARCH_FAILED, SS_SEARCH_FAILED,
SS_SEARCH_RESULTS_FETCHED_SPECIAL, SS_SEARCH_RESULTS_FETCHED_SPECIAL,
WANTED_COMICS_FETCHED, WANTED_COMICS_FETCHED,
VOLUMES_FETCHED,
CV_WEEKLY_PULLLIST_FETCHED,
} from "../constants/action-types"; } from "../constants/action-types";
import { success } from "react-notification-system-redux"; import { success } from "react-notification-system-redux";
import { removeLeadingPeriod } from "../shared/utils/formatting.utils"; import { removeLeadingPeriod } from "../shared/utils/formatting.utils";
@@ -194,6 +196,7 @@ export const fetchVolumeGroups = () => async (dispatch) => {
}; };
export const fetchComicVineMatches = export const fetchComicVineMatches =
(searchPayload, issueSearchQuery, seriesSearchQuery?) => async (dispatch) => { (searchPayload, issueSearchQuery, seriesSearchQuery?) => async (dispatch) => {
console.log(issueSearchQuery);
try { try {
dispatch({ dispatch({
type: CV_API_CALL_IN_PROGRESS, type: CV_API_CALL_IN_PROGRESS,
@@ -223,7 +226,6 @@ export const fetchComicVineMatches =
}, },
}) })
.then((response) => { .then((response) => {
console.log(response);
let matches: any = []; let matches: any = [];
if ( if (
!isNil(response.data.results) && !isNil(response.data.results) &&
@@ -233,7 +235,6 @@ export const fetchComicVineMatches =
} else { } else {
matches = response.data.map((match) => match); matches = response.data.map((match) => match);
} }
console.log(matches);
dispatch({ dispatch({
type: CV_SEARCH_SUCCESS, type: CV_SEARCH_SUCCESS,
searchResults: matches, searchResults: matches,
@@ -252,7 +253,13 @@ export const fetchComicVineMatches =
}); });
}; };
export const extractComicArchive = (path: string) => async (dispatch) => { /**
* This method is a proxy to `uncompressFullArchive` which uncompresses complete `rar` or `zip` archives
* @param {string} path The path to the compressed archive
* @param {any} options Options object
* @returns {any}
*/
export const extractComicArchive = (path: string, options: any) => async (dispatch) => {
const comicBookPages: string[] = []; const comicBookPages: string[] = [];
dispatch({ dispatch({
type: IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_IN_PROGRESS, type: IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_IN_PROGRESS,
@@ -317,12 +324,16 @@ export const searchIssue = (query, options) => async (dispatch) => {
data: response.data.body, data: response.data.body,
}); });
break; break;
case "volumesPage":
dispatch({
type: VOLUMES_FETCHED,
data: response.data.body,
});
break;
default: default:
break; break;
} }
}; };
export const analyzeImage = export const analyzeImage =
(imageFilePath: string | Buffer) => async (dispatch) => { (imageFilePath: string | Buffer) => async (dispatch) => {

View File

@@ -16,7 +16,6 @@ export const saveSettings =
method: "POST", method: "POST",
data: { settingsPayload, settingsObjectId }, data: { settingsPayload, settingsObjectId },
}); });
console.log(result.data);
dispatch({ dispatch({
type: SETTINGS_OBJECT_FETCHED, type: SETTINGS_OBJECT_FETCHED,
data: result.data, data: result.data,

View File

@@ -410,20 +410,34 @@ pre {
} }
} }
// Library // Library
.sticky { .header-area {
width: 100%;
padding: 25px 0 15px 0;
position: sticky; position: sticky;
top: 57px; z-index:9999;
z-index: 2;
background: #fffffc; background: #fffffc;
top: 50px;
} }
.library { .library {
.table-controls {
background: #fffffc;
justify-content: space-between;
position: sticky;
top: 126px;
padding-bottom: 10px;
}
.pagination {
margin: 0;
background: #fffffc;
}
table { table {
border-collapse: separate; border-collapse: separate;
width: 100%; width: 100%;
thead { thead {
position: sticky; position: sticky;
top: 146px; top: 250px;
z-index: 1; z-index: 1;
background: #fffffc; background: #fffffc;
min-height: 130px; min-height: 130px;

View File

@@ -4,11 +4,25 @@ import { useDispatch } from "react-redux";
import { saveSettings, deleteSettings } from "../../actions/settings.actions"; import { saveSettings, deleteSettings } from "../../actions/settings.actions";
import { AirDCPPSettingsConfirmation } from "./AirDCPPSettingsConfirmation"; import { AirDCPPSettingsConfirmation } from "./AirDCPPSettingsConfirmation";
import { AirDCPPSocketContext } from "../../context/AirDCPPSocket"; import { AirDCPPSocketContext } from "../../context/AirDCPPSocket";
import { isUndefined, isEmpty } from "lodash"; import { isUndefined, isEmpty, isNil } from "lodash";
export const AirDCPPSettingsForm = (): ReactElement => { export const AirDCPPSettingsForm = (): ReactElement => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const airDCPPSettings = useContext(AirDCPPSocketContext); const airDCPPSettings = useContext(AirDCPPSocketContext);
const hostValidator = (hostname: string): string | null => {
const hostnameRegex = /[\W]+/gm;
try {
if (!isUndefined(hostname)) {
const matches = hostname.match(hostnameRegex);
return (isNil(matches) && matches.length !== 0) ? hostname : "Invalid hostname; it should not contain special characters";
}
}
catch {
return null;
}
}
const onSubmit = useCallback(async (values) => { const onSubmit = useCallback(async (values) => {
try { try {
airDCPPSettings.setSettings(values); airDCPPSettings.setSettings(values);
@@ -25,7 +39,7 @@ export const AirDCPPSettingsForm = (): ReactElement => {
airDCPPSettings.setSettings({}); airDCPPSettings.setSettings({});
dispatch(deleteSettings()); dispatch(deleteSettings());
}, []); }, []);
const validate = async () => {}; const validate = async () => { };
const initFormData = !isUndefined( const initFormData = !isUndefined(
airDCPPSettings.airDCPPState.settings.directConnect, airDCPPSettings.airDCPPState.settings.directConnect,
) )
@@ -52,12 +66,24 @@ export const AirDCPPSettingsForm = (): ReactElement => {
</Field> </Field>
</span> </span>
</p> </p>
<p className="control is-expanded"> <div className="control is-expanded">
<Field <Field
name="hostname" name="hostname"
validate={hostValidator}>
{({ input, meta }) => (
<div>
<input {...input} type="text" placeholder="AirDC++ hostname" className="input" />
{meta.error && meta.touched && <span className="is-size-7 has-text-danger">{meta.error}</span>}
</div>
)}
</Field>
</div>
<p className="control">
<Field
name="port"
component="input" component="input"
className="input" className="input"
placeholder="AirDC++ host IP / hostname" placeholder="AirDC++ port"
/> />
</p> </p>
</div> </div>

View File

@@ -3,14 +3,11 @@ import Dashboard from "./Dashboard/Dashboard";
import Import from "./Import"; import Import from "./Import";
import { ComicDetailContainer } from "./ComicDetail/ComicDetailContainer"; import { ComicDetailContainer } from "./ComicDetail/ComicDetailContainer";
import LibraryContainer from "./Library/LibraryContainer"; import TabulatedContentContainer from "./Library/TabulatedContentContainer";
import LibraryGrid from "./Library/LibraryGrid"; import LibraryGrid from "./Library/LibraryGrid";
import Search from "./Search"; import Search from "./Search";
import Settings from "./Settings"; import Settings from "./Settings";
import VolumeDetail from "./VolumeDetail/VolumeDetail"; import VolumeDetail from "./VolumeDetail/VolumeDetail";
import PullList from "./PullList/PullList";
import WantedComics from "./WantedComics/WantedComics";
import Volumes from "./Volumes/Volumes";
import Downloads from "./Downloads/Downloads"; import Downloads from "./Downloads/Downloads";
import { Routes, Route } from "react-router-dom"; import { Routes, Route } from "react-router-dom";
@@ -23,15 +20,18 @@ import {
import { isEmpty, isUndefined } from "lodash"; import { isEmpty, isUndefined } from "lodash";
import { AIRDCPP_DOWNLOAD_PROGRESS_TICK } from "../constants/action-types"; import { AIRDCPP_DOWNLOAD_PROGRESS_TICK } from "../constants/action-types";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import axios from "axios";
import { LIBRARY_SERVICE_BASE_URI } from "../constants/endpoints";
import { useParams } from "react-router";
/**
* Method that initializes an AirDC++ socket connection
* 1. Initializes event listeners for download init, tick and complete events
* 2. Handles errors in case the connection to AirDC++ is not established or terminated
* @returns void
*/
const AirDCPPSocketComponent = (): ReactElement => { const AirDCPPSocketComponent = (): ReactElement => {
const airDCPPConfiguration = useContext(AirDCPPSocketContext); const airDCPPConfiguration = useContext(AirDCPPSocketContext);
const dispatch = useDispatch(); const dispatch = useDispatch();
useEffect(() => { useEffect(() => {
const foo = async () => { const initializeAirDCPPEventListeners = async () => {
if ( if (
!isUndefined(airDCPPConfiguration.airDCPPState) && !isUndefined(airDCPPConfiguration.airDCPPState) &&
!isEmpty(airDCPPConfiguration.airDCPPState.settings) && !isEmpty(airDCPPConfiguration.airDCPPState.settings) &&
@@ -79,7 +79,7 @@ const AirDCPPSocketComponent = (): ReactElement => {
); );
} }
}; };
foo(); initializeAirDCPPEventListeners();
}, [airDCPPConfiguration]); }, [airDCPPConfiguration]);
return <></>; return <></>;
}; };
@@ -92,7 +92,7 @@ export const App = (): ReactElement => {
<Routes> <Routes>
<Route path="/" element={<Dashboard />} /> <Route path="/" element={<Dashboard />} />
<Route path="/import" element={<Import path={"./comics"} />} /> <Route path="/import" element={<Import path={"./comics"} />} />
<Route path="/library" element={<LibraryContainer />} /> <Route path="/library" element={<TabulatedContentContainer category="library" />} />
<Route path="/library-grid" element={<LibraryGrid />} /> <Route path="/library-grid" element={<LibraryGrid />} />
<Route path="/downloads" element={<Downloads data={{}} />} /> <Route path="/downloads" element={<Downloads data={{}} />} />
<Route path="/search" element={<Search />} /> <Route path="/search" element={<Search />} />
@@ -105,9 +105,9 @@ export const App = (): ReactElement => {
element={<VolumeDetail />} element={<VolumeDetail />}
/> />
<Route path="/settings" element={<Settings />} /> <Route path="/settings" element={<Settings />} />
<Route path="/pull-list/all" element={<PullList />} /> <Route path="/pull-list/all" element={<TabulatedContentContainer category="pullList" />} />
<Route path="/wanted/all" element={<WantedComics />} /> <Route path="/wanted/all" element={<TabulatedContentContainer category="wanted" />} />
<Route path="/volumes/all" element={<Volumes />} /> <Route path="/volumes/all" element={<TabulatedContentContainer category="volumes" />} />
</Routes> </Routes>
</div> </div>
</AirDCPPSocketContextProvider> </AirDCPPSocketContextProvider>

View File

@@ -18,7 +18,6 @@ export const Menu = (props): ReactElement => {
} else if (!isEmpty(data.sourcedMetadata)) { } else if (!isEmpty(data.sourcedMetadata)) {
issueSearchQuery = refineQuery(data.sourcedMetadata.comicvine.name); issueSearchQuery = refineQuery(data.sourcedMetadata.comicvine.name);
} }
dispatch(fetchComicVineMatches(data, issueSearchQuery, seriesSearchQuery)); dispatch(fetchComicVineMatches(data, issueSearchQuery, seriesSearchQuery));
setSlidingPanelContentId("CVMatches"); setSlidingPanelContentId("CVMatches");
setVisible(true); setVisible(true);

View File

@@ -5,6 +5,7 @@ import Card from "../Carda";
import { ComicVineMatchPanel } from "./ComicVineMatchPanel"; import { ComicVineMatchPanel } from "./ComicVineMatchPanel";
import { RawFileDetails } from "./RawFileDetails"; import { RawFileDetails } from "./RawFileDetails";
import { ComicVineSearchForm } from "../ComicVineSearchForm";
import TabControls from "./TabControls"; import TabControls from "./TabControls";
import { EditMetadataPanel } from "./EditMetadataPanel"; import { EditMetadataPanel } from "./EditMetadataPanel";
@@ -40,6 +41,15 @@ type ComicDetailProps = {};
*/ */
export const ComicDetail = (data: ComicDetailProps): ReactElement => { export const ComicDetail = (data: ComicDetailProps): ReactElement => {
const {
data: {
_id,
rawFileDetails,
inferredMetadata,
sourcedMetadata: { comicvine, locg, comicInfo },
},
userSettings,
} = data;
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [slidingPanelContentId, setSlidingPanelContentId] = useState(""); const [slidingPanelContentId, setSlidingPanelContentId] = useState("");
@@ -67,9 +77,10 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
dispatch(extractComicArchive(filePath)); dispatch(extractComicArchive(filePath));
}, []); }, []);
const afterOpenModal = useCallback(() => { const afterOpenModal = useCallback((things) => {
// references are now sync'd and can be accessed. // references are now sync'd and can be accessed.
// subtitle.style.color = "#f00"; // subtitle.style.color = "#f00";
console.log(things);
}, []); }, []);
const closeModal = useCallback(() => { const closeModal = useCallback(() => {
@@ -79,9 +90,21 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
// sliding panel init // sliding panel init
const contentForSlidingPanel = { const contentForSlidingPanel = {
CVMatches: { CVMatches: {
content: () => { content: (props) => (
if (!comicVineAPICallProgress) { <>
return ( <div className="card search-criteria-card">
<div className="card-content">
<ComicVineSearchForm data={rawFileDetails}/>
</div>
</div>
<p className="is-size-5 mt-3 mb-2 ml-3">Searching for:</p>
{inferredMetadata.issue ? (
<div className="ml-3">
<span className="tag mr-3">{inferredMetadata.issue.name} </span>
<span className="tag"> # {inferredMetadata.issue.number} </span>
</div>
) : null}
{!comicVineAPICallProgress ? (
<ComicVineMatchPanel <ComicVineMatchPanel
props={{ props={{
comicVineSearchQueryObject, comicVineSearchQueryObject,
@@ -89,11 +112,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
comicVineSearchResults, comicVineSearchResults,
comicObjectId, comicObjectId,
}} }}
/> />) : (<div className="progress-indicator-container" >
);
} else {
return (
<div className="progress-indicator-container">
<div className="indicator"> <div className="indicator">
<Loader <Loader
type="MutatingDots" type="MutatingDots"
@@ -104,24 +123,16 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
visible={comicVineAPICallProgress} visible={comicVineAPICallProgress}
/> />
</div> </div>
</div> </div >)}
); </>),
}
},
}, },
editComicBookMetadata: { editComicBookMetadata: {
content: () => <EditMetadataPanel />, content: () => <EditMetadataPanel />,
}, },
}; };
const {
data: {
_id,
rawFileDetails,
inferredMetadata,
sourcedMetadata: { comicvine, locg, comicInfo },
},
userSettings,
} = data;
// check for the availability of CV metadata // check for the availability of CV metadata
const isComicBookMetadataAvailable = const isComicBookMetadataAvailable =

View File

@@ -10,38 +10,8 @@ export const ComicVineMatchPanel = (comicVineData): ReactElement => {
comicVineAPICallProgress, comicVineAPICallProgress,
comicVineSearchResults, comicVineSearchResults,
} = comicVineData.props; } = comicVineData.props;
console.log(comicVineData);
return ( return (
<> <>
{!isEmpty(comicVineSearchQueryObject) && (
<div className="card search-criteria-card">
<div className="card-content">
<ComicVineSearchForm />
<p className="is-size-6">Searching against:</p>
<div className="field is-grouped is-grouped-multiline">
<div className="control">
<div className="tags has-addons">
<span className="tag">Title</span>
<span className="tag is-info">
{comicVineSearchQueryObject.issue.inferredIssueDetails.name}
</span>
</div>
</div>
<div className="control">
<div className="tags has-addons">
<span className="tag">Number</span>
<span className="tag is-info">
{
comicVineSearchQueryObject.issue.inferredIssueDetails
.number
}
</span>
</div>
</div>
</div>
</div>
</div>
)}
<div className="search-results-container"> <div className="search-results-container">
{!isEmpty(comicVineSearchResults) && ( {!isEmpty(comicVineSearchResults) && (
<MatchResult <MatchResult

View File

@@ -1,22 +1,31 @@
import React from "react"; import React, { useCallback } from "react";
import { Form, Field } from "react-final-form"; import { Form, Field } from "react-final-form";
import Collapsible from "react-collapsible"; import Collapsible from "react-collapsible";
import { fetchComicVineMatches } from "../actions/fileops.actions";
import { useDispatch } from "react-redux";
/** /**
* Component for accepting ComicVine search parameters * Component for performing search against ComicVine
* *
* @component * @component
* @example * @example
* const age = 21
* const name = 'Jitendra Nirnejak'
* return ( * return (
* <User age={age} name={name} /> * <ComicVineSearchForm data={rawFileDetails} />
* ) * )
*/ */
export const ComicVineSearchForm = () => { export const ComicVineSearchForm = (data) => {
const onSubmit = () => { const dispatch = useDispatch();
return true; const onSubmit = useCallback((value) => {
}; const userInititatedQuery = {
inferredIssueDetails: {
name: value.issueName,
number: value.issueNumber,
subtitle: "",
year: value.issueYear,
},
};
dispatch(fetchComicVineMatches(data, userInititatedQuery));
}, []);
const validate = () => { const validate = () => {
return true; return true;
}; };
@@ -28,7 +37,7 @@ export const ComicVineSearchForm = () => {
render={({ handleSubmit }) => ( render={({ handleSubmit }) => (
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<span className="field is-normal"> <span className="field is-normal">
<label className="label">Issue Details</label> <label className="label mb-2 is-size-5">Search Manually</label>
</span> </span>
<div className="field is-horizontal"> <div className="field is-horizontal">
<div className="field-body"> <div className="field-body">
@@ -48,11 +57,15 @@ export const ComicVineSearchForm = () => {
)} )}
</Field> </Field>
</div> </div>
</div>
</div>
<div className="field is-horizontal">
<div className="field-body">
<div className="field"> <div className="field">
<Field name="issueNumber"> <Field name="issueNumber">
{(props) => ( {(props) => (
<p className="control is-expanded has-icons-left"> <p className="control has-icons-left">
<input <input
{...props.input} {...props.input}
className="input is-normal" className="input is-normal"
@@ -65,18 +78,36 @@ export const ComicVineSearchForm = () => {
)} )}
</Field> </Field>
</div> </div>
<div className="field">
<Field name="issueYear">
{(props) => (
<p className="control has-icons-left">
<input
{...props.input}
className="input is-normal"
placeholder="Type the issue year"
/>
<span className="icon is-small is-left">
<i className="fas fa-hashtag"></i>
</span>
</p>
)}
</Field>
</div>
</div> </div>
</div> </div>
<div className="field is-horizontal"> <div className="field is-horizontal">
<div className="field-body"> <div className="field-body">
<div className="field"> <div className="field">
<div className="control"> <div className="control">
<button <button
type="submit" type="submit"
className="button is-info is-light is-outlined is-small" className="button is-success is-light is-outlined is-small"
> >
<span className="icon"> <span className="icon">
<i className="fas fa-hand-sparkles"></i> <i className="fas fa-search"></i>
</span> </span>
<span>Search</span> <span>Search</span>
</button> </button>
@@ -89,16 +120,7 @@ export const ComicVineSearchForm = () => {
/> />
); );
return ( return <MyForm />;
<Collapsible
trigger={"Match Manually"}
triggerTagName="a"
triggerClassName={"is-size-6"}
triggerOpenedClassName={"is-size-6"}
>
<MyForm />
</Collapsible>
);
}; };
export default ComicVineSearchForm; export default ComicVineSearchForm;

View File

@@ -20,7 +20,7 @@ export const PullList = ({ issues }: PullListProps): ReactElement => {
useEffect(() => { useEffect(() => {
dispatch( dispatch(
getWeeklyPullList({ getWeeklyPullList({
startDate: "2022-8-9", startDate: "2022-11-15",
pageSize: "15", pageSize: "15",
currentPage: "1", currentPage: "1",
}), }),
@@ -127,7 +127,7 @@ export const PullList = ({ issues }: PullListProps): ReactElement => {
<Slider {...settings} ref={(c) => (sliderRef = c)}> <Slider {...settings} ref={(c) => (sliderRef = c)}>
{!isNil(pullList) && {!isNil(pullList) &&
pullList && pullList &&
map(pullList, (issue, idx) => { map(pullList, ({issue}, idx) => {
return ( return (
<Card <Card
key={idx} key={idx}

View File

@@ -1,22 +1,46 @@
import React, { useMemo, ReactElement, useCallback } from "react"; import React, { useMemo, ReactElement, useCallback, useEffect } from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import T2Table from "../shared/T2Table";
import { isEmpty, isNil, isUndefined } from "lodash"; import { isEmpty, isNil, isUndefined } from "lodash";
import MetadataPanel from "../shared/MetadataPanel"; import MetadataPanel from "../shared/MetadataPanel";
import SearchBar from "./SearchBar"; import T2Table from "../shared/T2Table";
import { useDispatch } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { searchIssue } from "../../actions/fileops.actions"; import { searchIssue } from "../../actions/fileops.actions";
import ellipsize from "ellipsize"; import ellipsize from "ellipsize";
interface IComicBookLibraryProps {
data: {
searchResults: any;
};
}
export const Library = (data: IComicBookLibraryProps): ReactElement => { /**
const { searchResults } = data.data; * Component that tabulates the contents of the user's ThreeTwo Library.
*
* @component
* @example
* <Library />
*/
export const Library = (): ReactElement => {
const searchResults = useSelector(
(state: RootState) => state.fileOps.libraryComics,
);
const searchError = useSelector(
(state: RootState) => state.fileOps.librarySearchError,
);
const dispatch = useDispatch();
useEffect(() => {
dispatch(
searchIssue(
{
query: {},
},
{
pagination: {
size: 15,
from: 0,
},
type: "all",
trigger: "libraryPage"
},
),
);
}, []);
// programatically navigate to comic detail // programatically navigate to comic detail
const navigate = useNavigate(); const navigate = useNavigate();
@@ -65,69 +89,73 @@ export const Library = (data: IComicBookLibraryProps): ReactElement => {
const WantedStatus = ({ value }) => { const WantedStatus = ({ value }) => {
return !value ? <span className="tag is-info is-light">Wanted</span> : null; return !value ? <span className="tag is-info is-light">Wanted</span> : null;
}; };
const columns = [ const columns = useMemo(() => [
{ {
header: "Comic Metadata", header: "Comic Metadata",
footer: 1, footer: 1,
columns: [ columns: [
{ {
header: "File Details", header: "File Details",
id: "fileDetails", id: "fileDetails",
minWidth: 400, minWidth: 400,
accessorKey: "_source", accessorKey: "_source",
cell: info => { cell: info => {
return <MetadataPanel data={info.getValue()} />; return <MetadataPanel data={info.getValue()} />;
},
}, },
{ },
header: "ComicInfo.xml", {
accessorKey: "_source.sourcedMetadata.comicInfo", header: "ComicInfo.xml",
align: "center", accessorKey: "_source.sourcedMetadata.comicInfo",
minWidth: 250, align: "center",
cell: info => minWidth: 250,
!isEmpty(info.getValue()) ? ( cell: info =>
<ComicInfoXML data={info.getValue()} /> !isEmpty(info.getValue()) ? (
) : ( <ComicInfoXML data={info.getValue()} />
<span className="tag">No ComicInfo.xml</span> ) : (
), <span className="tag mt-5">No ComicInfo.xml</span>
),
},
],
},
{
header: "Additional Metadata",
columns: [
{
header: "Publisher",
accessorKey:
"_source.sourcedMetadata.comicvine.volumeInformation",
cell: info => {
return (
!isNil(info.getValue()) && (
<h6 className="is-size-7 has-text-weight-bold">
{info.getValue().publisher.name}
</h6>
)
);
}, },
], },
}, {
{ header: "Something",
header: "Additional Metadata", accessorKey: "_source.acquisition.source.wanted",
columns: [ cell: info => {
{ !isUndefined(info.getValue()) ?
header: "Publisher",
accessorKey:
"_source.sourcedMetadata.comicvine.volumeInformation",
cell: info => {
return (
!isNil(info.getValue()) && (
<h6 className="is-size-7 has-text-weight-bold">
{ info.getValue().publisher.name }
</h6>
)
);
},
},
{
header: "Something",
accessorKey: "_source.acquisition.source.wanted",
cell: info => {
!isUndefined(info.getValue()) ?
<WantedStatus value={info.getValue().toString()} /> : "Nothing"; <WantedStatus value={info.getValue().toString()} /> : "Nothing";
},
}, },
], },
} ],
] }
], []);
// ImportStatus.propTypes = {
// value: PropTypes.bool.isRequired,
// };
const dispatch = useDispatch(); /**
const goToNextPage = useCallback((pageIndex, pageSize) => { * Pagination control that fetches the next x (pageSize) items
* based on the y (pageIndex) offset from the ThreeTwo Elasticsearch index
* @param {number} pageIndex
* @param {number} pageSize
* @returns void
*
**/
const nextPage = useCallback((pageIndex: number, pageSize: number) => {
dispatch( dispatch(
searchIssue( searchIssue(
{ {
@@ -139,17 +167,26 @@ export const Library = (data: IComicBookLibraryProps): ReactElement => {
from: pageSize * pageIndex + 1, from: pageSize * pageIndex + 1,
}, },
type: "all", type: "all",
trigger: "libraryPage",
}, },
), ),
); );
}, []); }, []);
const goToPreviousPage = useCallback((pageIndex, pageSize) => {
/**
* Pagination control that fetches the previous x (pageSize) items
* based on the y (pageIndex) offset from the ThreeTwo Elasticsearch index
* @param {number} pageIndex
* @param {number} pageSize
* @returns void
**/
const previousPage = useCallback((pageIndex: number, pageSize: number) => {
let from = 0; let from = 0;
if (pageIndex === 2) { if (pageIndex === 2) {
from = (pageIndex - 1) * pageSize + 2 - 27; from = (pageIndex - 1) * pageSize + 2 - 17;
} else { } else {
from = (pageIndex - 1) * pageSize + 2 - 26; from = (pageIndex - 1) * pageSize + 2 - 16;
} }
dispatch( dispatch(
searchIssue( searchIssue(
@@ -162,30 +199,35 @@ export const Library = (data: IComicBookLibraryProps): ReactElement => {
from, from,
}, },
type: "all", type: "all",
trigger: "libraryPage"
}, },
), ),
); );
}, []); }, []);
// ImportStatus.propTypes = {
// value: PropTypes.bool.isRequired,
// };
return ( return (
<section className="container"> <section className="container">
<div className="section"> <div className="section">
<h1 className="title">Library</h1> <div className="header-area">
{/* Search bar */} <h1 className="title">Library</h1>
<SearchBar /> </div>
{!isUndefined(searchResults) && ( {!isUndefined(searchResults.hits) && (
<div> <div>
<div className="library"> <div className="library">
<T2Table <T2Table
rowData={searchResults.hits.hits}
totalPages={searchResults.hits.total.value} totalPages={searchResults.hits.total.value}
columns={columns} columns={columns}
paginationHandlers={{ sourceData={searchResults?.hits?.hits}
nextPage: goToNextPage,
previousPage: goToPreviousPage,
}}
rowClickHandler={navigateToComicDetail} rowClickHandler={navigateToComicDetail}
paginationHandlers={{
nextPage,
previousPage,
}}
/> />
{/* pagination controls */}
</div> </div>
</div> </div>
)} )}

View File

@@ -1,63 +0,0 @@
import { isEmpty, isUndefined } from "lodash";
import React, { ReactElement, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { searchIssue } from "../../actions/fileops.actions";
import { Library } from "./Library";
const LibraryContainer = (): ReactElement => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(
searchIssue(
{
query: {},
},
{
pagination: {
size: 25,
from: 0,
},
type: "all",
trigger: "libraryPage"
},
),
);
}, []);
const searchResults = useSelector(
(state: RootState) => state.fileOps.libraryComics,
);
const searchError = useSelector(
(state: RootState) => state.fileOps.librarySearchError,
);
return !isEmpty(searchResults) ? (
<Library data={{ searchResults }} />
) : (
<div className="container">
<section className="section is-small">
<div className="columns">
<div className="column is-two-thirds">
<article className="message is-link">
<div className="message-body">
No comics were found in the library, Elasticsearch reports no
indices. Try importing a few comics into the library and come
back.
</div>
</article>
<pre>
{!isUndefined(searchError.data) &&
JSON.stringify(
searchError.data.meta.body.error.root_cause,
null,
4,
)}
</pre>
</div>
</div>
</section>
</div>
);
};
export default LibraryContainer;

View File

@@ -21,19 +21,20 @@ export const SearchBar = (): ReactElement => {
from: 0, from: 0,
}, },
type: "volumeName", type: "volumeName",
trigger: "libraryPage",
}, },
), ),
); );
}, []); }, []);
return ( return (
<div className="box sticky"> <div className="box">
<Form <Form
onSubmit={handleSubmit} onSubmit={handleSubmit}
initialValues={{}} initialValues={{}}
render={({ handleSubmit, form, submitting, pristine, values }) => ( render={({ handleSubmit, form, submitting, pristine, values }) => (
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="field is-grouped"> <div className="field is-grouped">
<div className="control is-expanded search"> <div className="control search is-expanded">
<Field name="search"> <Field name="search">
{({ input, meta }) => { {({ input, meta }) => {
return ( return (
@@ -55,26 +56,7 @@ export const SearchBar = (): ReactElement => {
</form> </form>
)} )}
/> />
{/* <div className="column one-fifth">
<div className="field has-addons">
<p className="control">
<button className="button">
<span className="icon is-small">
<i className="fa-solid fa-list"></i>
</span>
</button>
</p>
<p className="control">
<button className="button">
<Link to="/library-grid">
<span className="icon is-small">
<i className="fa-solid fa-image"></i>
</span>
</Link>
</button>
</p>
</div>
</div> */}
</div> </div>
); );
}; };

View File

@@ -0,0 +1,66 @@
import React, { ReactElement } from "react";
import PullList from "../PullList/PullList";
import { Volumes } from "../Volumes/Volumes";
import WantedComics from "../WantedComics/WantedComics";
import { Library } from "./Library";
interface ITabulatedContentContainerProps {
category: string;
}
/**
* Component to draw the contents of a category in a table.
*
* @component
* @example
* return (
* <TabulatedContentContainer category={"library"} />
* )
*/
const TabulatedContentContainer = (
props: ITabulatedContentContainerProps,
): ReactElement => {
const { category } = props;
const renderTabulatedContent = () => {
switch (category) {
case "library":
return <Library />;
case "pullList":
return <PullList />;
case "wanted":
return <WantedComics />;
case "volumes":
return <Volumes />;
default:
return <></>;
}
};
return renderTabulatedContent();
// : (
// <div className="container">
// <section className="section is-small">
// <div className="columns">
// <div className="column is-two-thirds">
// <article className="message is-link">
// <div className="message-body">
// No comics were found in the library, and Elasticsearch doesn't have any
// indices. Try resetting the library from <code>Settings > Flush DB & Temporary Folders</code> and then import your library again.
// </div>
// </article>
// <pre>
// {!isUndefined(searchError.data) &&
// JSON.stringify(
// searchError.data.meta.body.error.root_cause,
// null,
// 4,
// )}
// </pre>
// </div>
// </div>
// </section>
// </div>
// );
};
export default TabulatedContentContainer;

View File

@@ -4,18 +4,19 @@ import { getWeeklyPullList } from "../../actions/comicinfo.actions";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import Card from "../Carda"; import Card from "../Carda";
import ellipsize from "ellipsize"; import ellipsize from "ellipsize";
import { isNil } from "lodash";
export const PullList = (): ReactElement => { export const PullList = (): ReactElement => {
const pullListComics = useSelector( const pullListComics = useSelector(
(state: RootState) => state.comicInfo.pullList, (state: RootState) => state.comicInfo.pullList,
); );
console.log(pullListComics);
const dispatch = useDispatch(); const dispatch = useDispatch();
useEffect(() => { useEffect(() => {
dispatch( dispatch(
getWeeklyPullList({ getWeeklyPullList({
startDate: "2022-6-1", startDate: "2022-11-15",
pageSize: "100", pageSize: "15",
currentPage: "1", currentPage: "1",
}), }),
); );
@@ -25,24 +26,25 @@ export const PullList = (): ReactElement => {
const columnData = useMemo( const columnData = useMemo(
() => [ () => [
{ {
Header: "Comic Information", header: "Comic Information",
columns: [ columns: [
{ {
Header: "Details", header: "Details",
id: "comicDetails", id: "comicDetails",
minWidth: 450, minWidth: 450,
accessor: (row) => { accessorKey: "issue",
console.log(row); cell: (row) => {
const item = row.getValue();
return ( return (
<div className="columns"> <div className="columns">
<div className="column"> <div className="column is-three-quarters">
<div className="comic-detail issue-metadata"> <div className="comic-detail issue-metadata">
<dl> <dl>
<dd> <dd>
<div className="columns mt-2"> <div className="columns mt-2">
<div className="column is-3"> <div className="column is-3">
<Card <Card
imageUrl={row.cover} imageUrl={item.cover}
orientation={"vertical"} orientation={"vertical"}
hasDetails={false} hasDetails={false}
// cardContainerStyle={{ maxWidth: 200 }} // cardContainerStyle={{ maxWidth: 200 }}
@@ -52,18 +54,20 @@ export const PullList = (): ReactElement => {
<dl> <dl>
<dt> <dt>
<h6 className="name has-text-weight-medium mb-1"> <h6 className="name has-text-weight-medium mb-1">
{row.name} {item.name}
</h6> </h6>
</dt> </dt>
<dd className="is-size-7"> <dd className="is-size-7">
published by{" "} published by{" "}
<span className="has-text-weight-semibold"> <span className="has-text-weight-semibold">
{row.publisher} {item.publisher}
</span> </span>
</dd> </dd>
<dd className="is-size-7"> <dd className="is-size-7">
<span>{ellipsize(row.description, 190)}</span> <span>
{ellipsize(item.description, 190)}
</span>
</dd> </dd>
<dd className="is-size-7 mt-2"> <dd className="is-size-7 mt-2">
@@ -71,10 +75,10 @@ export const PullList = (): ReactElement => {
<div className="control"> <div className="control">
<span className="tags"> <span className="tags">
<span className="tag is-success is-light has-text-weight-semibold"> <span className="tag is-success is-light has-text-weight-semibold">
{row.price} {item.price}
</span> </span>
<span className="tag is-success is-light"> <span className="tag is-success is-light">
{row.pulls} {item.pulls}
</span> </span>
</span> </span>
</div> </div>
@@ -99,16 +103,24 @@ export const PullList = (): ReactElement => {
return ( return (
<section className="container"> <section className="container">
<div className="section"> <div className="section">
<h1 className="title">Weekly Pull List</h1> <div className="header-area">
<T2Table <h1 className="title">Weekly Pull List</h1>
rowData={pullListComics} </div>
columns={columnData} {!isNil(pullListComics) && (
totalPages={pullListComics.length} <div>
paginationHandlers={{ <div className="library">
nextPage: nextPageHandler, <T2Table
previousPage: previousPageHandler, sourceData={pullListComics}
}} columns={columnData}
/> totalPages={pullListComics.length}
paginationHandlers={{
nextPage: nextPageHandler,
previousPage: previousPageHandler,
}}
/>
</div>
</div>
)}
</div> </div>
</section> </section>
); );

View File

@@ -2,16 +2,13 @@ import React, { ReactElement, useEffect, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { searchIssue } from "../../actions/fileops.actions"; import { searchIssue } from "../../actions/fileops.actions";
import Card from "../Carda"; import Card from "../Carda";
import SearchBar from "../Library/SearchBar";
import T2Table from "../shared/T2Table"; import T2Table from "../shared/T2Table";
import ellipsize from "ellipsize"; import ellipsize from "ellipsize";
import { isUndefined } from "lodash";
import { convert } from "html-to-text"; import { convert } from "html-to-text";
import { isUndefined } from "lodash";
export const Volumes = (props): ReactElement => { export const Volumes = (props): ReactElement => {
const volumes = useSelector( const volumes = useSelector((state: RootState) => state.fileOps.volumes);
(state: RootState) => state.fileOps.librarySearchResults,
);
const dispatch = useDispatch(); const dispatch = useDispatch();
useEffect(() => { useEffect(() => {
dispatch( dispatch(
@@ -25,19 +22,20 @@ export const Volumes = (props): ReactElement => {
from: 0, from: 0,
}, },
type: "volumes", type: "volumes",
trigger: "volumesPage",
}, },
), ),
); );
}, []); }, []);
console.log(volumes);
const columnData = useMemo( const columnData = useMemo(
() => [ () => [
{ {
Header: "Volume Details", header: "Volume Details",
id: "volumeDetails", id: "volumeDetails",
minWidth: 450, minWidth: 450,
accessor: (row) => { accessorKey: "_source",
cell: (row) => {
const foo = row.getValue();
return ( return (
<div className="columns"> <div className="columns">
<div className="column"> <div className="column">
@@ -45,11 +43,11 @@ export const Volumes = (props): ReactElement => {
<dl> <dl>
<dd> <dd>
<div className="columns mt-2"> <div className="columns mt-2">
<div className="column is-3"> <div className="">
<Card <Card
imageUrl={ imageUrl={
row._source.sourcedMetadata.comicvine foo.sourcedMetadata.comicvine.volumeInformation
.volumeInformation.image.thumb_url .image.thumb_url
} }
orientation={"vertical"} orientation={"vertical"}
hasDetails={false} hasDetails={false}
@@ -61,7 +59,7 @@ export const Volumes = (props): ReactElement => {
<dt> <dt>
<h6 className="name has-text-weight-medium mb-1"> <h6 className="name has-text-weight-medium mb-1">
{ {
row._source.sourcedMetadata.comicvine foo.sourcedMetadata.comicvine
.volumeInformation.name .volumeInformation.name
} }
</h6> </h6>
@@ -70,7 +68,7 @@ export const Volumes = (props): ReactElement => {
published by{" "} published by{" "}
<span className="has-text-weight-semibold"> <span className="has-text-weight-semibold">
{ {
row._source.sourcedMetadata.comicvine foo.sourcedMetadata.comicvine
.volumeInformation.publisher.name .volumeInformation.publisher.name
} }
</span> </span>
@@ -80,7 +78,7 @@ export const Volumes = (props): ReactElement => {
<span> <span>
{ellipsize( {ellipsize(
convert( convert(
row._source.sourcedMetadata.comicvine foo.sourcedMetadata.comicvine
.volumeInformation.description, .volumeInformation.description,
{ {
baseElements: { baseElements: {
@@ -102,7 +100,7 @@ export const Volumes = (props): ReactElement => {
</span> </span>
<span className="tag is-success is-light"> <span className="tag is-success is-light">
{ {
row._source.sourcedMetadata.comicvine foo.sourcedMetadata.comicvine
.volumeInformation.count_of_issues .volumeInformation.count_of_issues
} }
</span> </span>
@@ -122,36 +120,35 @@ export const Volumes = (props): ReactElement => {
}, },
}, },
{ {
Header: "Download Status", header: "Download Status",
columns: [ columns: [
{ {
Header: "Files", header: "Files",
accessor: "_source.acquisition.directconnect", accessorKey: "_source.acquisition.directconnect",
align: "right", align: "right",
Cell: (props) => { cell: (props) => {
const row = props.getValue();
return ( return (
<div <div
style={{ style={{
display: "flex", display: "flex",
// flexDirection: "column", flexDirection: "column",
justifyContent: "center", justifyContent: "center",
}} }}
> >
{props.cell.value.length > 0 ? ( {row.length > 0 ? (
<span className="tag is-warning"> <span className="tag is-warning">{row.length}</span>
{props.cell.value.length}
</span>
) : null} ) : null}
</div> </div>
); );
}, },
}, },
{ {
Header: "Type", header: "Type",
id: "Air", id: "Air",
}, },
{ {
Header: "Type", header: "Type",
id: "dcc", id: "dcc",
}, },
], ],
@@ -162,16 +159,24 @@ export const Volumes = (props): ReactElement => {
return ( return (
<section className="container"> <section className="container">
<div className="section"> <div className="section">
{volumes.hits ? ( <div className="header-area">
<h1 className="title">Volumes</h1>
</div>
{!isUndefined(volumes.hits) && (
<div> <div>
<div className="library"> <div className="library">
<h1 className="title">Volumes</h1> <T2Table
{/* Search bar */} sourceData={volumes?.hits?.hits}
<SearchBar /> totalPages={volumes.hits.hits.length}
<T2Table rowData={volumes.hits.hits} columns={columnData} /> paginationHandlers={{
nextPage: () => {},
previousPage: () => {},
}}
columns={columnData}
/>
</div> </div>
</div> </div>
) : null} )}
</div> </div>
</section> </section>
); );

View File

@@ -1,4 +1,4 @@
import React, { ReactElement, useEffect, useMemo } from "react"; import React, { ReactElement, useCallback, useEffect, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { searchIssue } from "../../actions/fileops.actions"; import { searchIssue } from "../../actions/fileops.actions";
import SearchBar from "../Library/SearchBar"; import SearchBar from "../Library/SearchBar";
@@ -86,23 +86,81 @@ export const WantedComics = (props): ReactElement => {
}, },
]; ];
/**
* Pagination control that fetches the next x (pageSize) items
* based on the y (pageIndex) offset from the Elasticsearch index
* @param {number} pageIndex
* @param {number} pageSize
* @returns void
*
**/
const nextPage = useCallback((pageIndex: number, pageSize: number) => {
dispatch(
searchIssue(
{
query: {},
},
{
pagination: {
size: pageSize,
from: pageSize * pageIndex + 1,
},
type: "wanted",
trigger: "wantedComicsPage",
},
),
);
}, []);
/**
* Pagination control that fetches the previous x (pageSize) items
* based on the y (pageIndex) offset from the Elasticsearch index
* @param {number} pageIndex
* @param {number} pageSize
* @returns void
**/
const previousPage = useCallback((pageIndex: number, pageSize: number) => {
let from = 0;
if (pageIndex === 2) {
from = (pageIndex - 1) * pageSize + 2 - 17;
} else {
from = (pageIndex - 1) * pageSize + 2 - 16;
}
dispatch(
searchIssue(
{
query: {},
},
{
pagination: {
size: pageSize,
from,
},
type: "wanted",
trigger: "wantedComicsPage"
},
),
);
}, []);
return ( return (
<section className="container"> <section className="container">
<div className="section"> <div className="section">
<h1 className="title">Wanted Comics</h1> <div className="header-area">
{/* Search bar */} <h1 className="title">Wanted Comics</h1>
<SearchBar /> </div>
{!isEmpty(wantedComics) && ( {!isEmpty(wantedComics) && (
<div> <div>
<div className="library"> <div className="library">
<T2Table <T2Table
rowData={wantedComics} sourceData={wantedComics}
totalPages={wantedComics.length} totalPages={wantedComics.length}
columns={columnData} columns={columnData}
// paginationHandlers={{ paginationHandlers={{
// nextPage: goToNextPage, nextPage: nextPage,
// previousPage: goToPreviousPage, previousPage: previousPage,
// }} }}
// rowClickHandler={navigateToComicDetail} // rowClickHandler={navigateToComicDetail}
/> />
{/* pagination controls */} {/* pagination controls */}

View File

@@ -77,7 +77,9 @@ export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => {
</div> </div>
)} )}
</div> </div>
</div> </div>
</dd> </dd>
</dl> </dl>
), ),
@@ -211,7 +213,7 @@ export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => {
orientation={"vertical"} orientation={"vertical"}
hasDetails={false} hasDetails={false}
imageStyle={props.imageStyle} imageStyle={props.imageStyle}
// cardContainerStyle={{ maxWidth: 200 }} // cardContainerStyle={{ maxWidth: 200 }}
/> />
</div> </div>
<div className="column">{metadataPanel.content()}</div> <div className="column">{metadataPanel.content()}</div>

View File

@@ -1,27 +1,114 @@
import React, { ReactElement, useState } from "react"; import React, { ReactElement, useMemo, useState } from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import SearchBar from "../Library/SearchBar";
import { Link } from "react-router-dom";
import { import {
ColumnDef, ColumnDef,
flexRender, flexRender,
getCoreRowModel, getCoreRowModel,
getFilteredRowModel,
useReactTable, useReactTable,
PaginationState,
} from "@tanstack/react-table"; } from "@tanstack/react-table";
export const T2Table = (tableOptions): ReactElement => { export const T2Table = (tableOptions): ReactElement => {
const { rowData, columns, paginationHandlers, totalPages, rowClickHandler } = const { sourceData, columns, paginationHandlers: { nextPage, previousPage }, totalPages, rowClickHandler } =
tableOptions; tableOptions;
const [isPageSizeDropdownCollapsed, collapsePageSizeDropdown] =
useState(false); const [{ pageIndex, pageSize }, setPagination] =
const togglePageSizeDropdown = () => useState<PaginationState>({
collapsePageSizeDropdown(!isPageSizeDropdownCollapsed); pageIndex: 1,
pageSize: 15,
});
console.log(sourceData)
const pagination = useMemo(
() => ({
pageIndex,
pageSize,
}),
[pageIndex, pageSize]
);
/**
* Pagination control to move forward one page
* @returns void
*/
const goToNextPage = () => {
setPagination({
pageIndex: pageIndex + 1,
pageSize,
});
nextPage(pageIndex, pageSize);
}
/**
* Pagination control to move backward one page
* @returns void
**/
const goToPreviousPage = () => {
setPagination({
pageIndex: pageIndex - 1,
pageSize,
});
previousPage(pageIndex, pageSize);
}
const table = useReactTable({ const table = useReactTable({
data: rowData, data: sourceData,
columns, columns,
manualPagination: true,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
pageCount: sourceData.length ?? -1,
state: {
pagination,
},
onPaginationChange: setPagination,
}); });
return ( return (
<> <>
<div className="columns table-controls">
{/* Search bar */}
<div className="column is-half">
<SearchBar />
</div>
{/* pagination controls */}
<nav className="pagination columns">
<div className="mr-4 has-text-weight-semibold has-text-left">
<p className="is-size-5">Page {pageIndex} of {Math.ceil(totalPages / pageSize)}</p>
{/* <p>{totalPages} comics in all</p> */}
</div>
<div className="field has-addons">
<p className="control">
<div className="button" onClick={() => goToPreviousPage()}> <i className="fas fa-chevron-left"></i></div>
</p>
<p className="control">
<div className="button" onClick={() => goToNextPage()}> <i className="fas fa-chevron-right"></i> </div>
</p>
<div className="field has-addons ml-5">
<p className="control">
<button className="button">
<span className="icon is-small">
<i className="fa-solid fa-list"></i>
</span>
</button>
</p>
<p className="control">
<button className="button">
<Link to="/library-grid">
<span className="icon is-small">
<i className="fa-solid fa-image"></i>
</span>
</Link>
</button>
</p>
</div>
</div>
</nav>
</div>
<table className="table is-hoverable"> <table className="table is-hoverable">
<thead> <thead>
{table.getHeaderGroups().map((headerGroup, idx) => ( {table.getHeaderGroups().map((headerGroup, idx) => (
@@ -60,15 +147,12 @@ export const T2Table = (tableOptions): ReactElement => {
})} })}
</tbody> </tbody>
</table> </table>
{/* pagination control */}
</> </>
); );
}; };
T2Table.propTypes = { T2Table.propTypes = {
rowData: PropTypes.array, sourceData: PropTypes.array,
totalPages: PropTypes.number, totalPages: PropTypes.number,
columns: PropTypes.array, columns: PropTypes.array,
paginationHandlers: PropTypes.shape({ paginationHandlers: PropTypes.shape({

View File

@@ -51,6 +51,7 @@ export const IMS_COMIC_BOOK_GROUPS_CALL_IN_PROGRESS =
"IMS_COMIC_BOOK_GROUPS_CALL_IN_PROGRESS"; "IMS_COMIC_BOOK_GROUPS_CALL_IN_PROGRESS";
export const IMS_COMIC_BOOK_GROUPS_CALL_FAILED = export const IMS_COMIC_BOOK_GROUPS_CALL_FAILED =
"IMS_COMIC_BOOK_GROUPS_CALL_FAILED"; "IMS_COMIC_BOOK_GROUPS_CALL_FAILED";
export const VOLUMES_FETCHED="VOLUMES_FETCHED";
// search results from the Search service // search results from the Search service
export const SS_SEARCH_RESULTS_FETCHED = "SS_SEARCH_RESULTS_FETCHED"; export const SS_SEARCH_RESULTS_FETCHED = "SS_SEARCH_RESULTS_FETCHED";

View File

@@ -53,7 +53,7 @@ const AirDCPPSocketContextProvider = ({ children }) => {
} = configuration; } = configuration;
const initializedAirDCPPSocket = new AirDCPPSocket({ const initializedAirDCPPSocket = new AirDCPPSocket({
protocol: `${host.protocol}`, protocol: `${host.protocol}`,
hostname: `${host.hostname}`, hostname: `${host.hostname}:${host.port}`,
}); });
const socketConnectionInformation = await initializedAirDCPPSocket.connect( const socketConnectionInformation = await initializedAirDCPPSocket.connect(

View File

@@ -109,10 +109,14 @@ function comicinfoReducer(state = initialState, action) {
...state, ...state,
}; };
case CV_WEEKLY_PULLLIST_FETCHED: { case CV_WEEKLY_PULLLIST_FETCHED: {
const foo = [];
action.data.map((item) => {
foo.push({issue: item})
});
return { return {
...state, ...state,
inProgress: false, inProgress: false,
pullList: [...action.data], pullList: foo,
}; };
} }
case LIBRARY_STATISTICS_CALL_IN_PROGRESS: case LIBRARY_STATISTICS_CALL_IN_PROGRESS:

View File

@@ -28,6 +28,7 @@ import {
LS_IMPORT_CALL_IN_PROGRESS, LS_IMPORT_CALL_IN_PROGRESS,
SS_SEARCH_FAILED, SS_SEARCH_FAILED,
SS_SEARCH_RESULTS_FETCHED_SPECIAL, SS_SEARCH_RESULTS_FETCHED_SPECIAL,
VOLUMES_FETCHED,
} from "../constants/action-types"; } from "../constants/action-types";
const initialState = { const initialState = {
IMSCallInProgress: false, IMSCallInProgress: false,
@@ -45,6 +46,7 @@ const initialState = {
recentComics: [], recentComics: [],
wantedComics: [], wantedComics: [],
libraryComics: [], libraryComics: [],
volumes: [],
librarySearchResultsFormatted: [], librarySearchResultsFormatted: [],
librarySearchResultCount: 0, librarySearchResultCount: 0,
libraryQueueResults: [], libraryQueueResults: [],
@@ -196,7 +198,6 @@ function fileOpsReducer(state = initialState, action) {
} }
case SS_SEARCH_RESULTS_FETCHED_SPECIAL: { case SS_SEARCH_RESULTS_FETCHED_SPECIAL: {
const foo = []; const foo = [];
console.log(action.data.hits)
if (!isUndefined(action.data.hits)) { if (!isUndefined(action.data.hits)) {
map(action.data.hits.hits, ({ _source }) => { map(action.data.hits.hits, ({ _source }) => {
foo.push(_source); foo.push(_source);
@@ -221,6 +222,13 @@ function fileOpsReducer(state = initialState, action) {
SSCallInProgress: false, SSCallInProgress: false,
}; };
} }
case VOLUMES_FETCHED:
return {
...state,
volumes: action.data,
SSCallInProgress: false,
};
case SS_SEARCH_FAILED: { case SS_SEARCH_FAILED: {
return { return {

View File

@@ -4030,9 +4030,9 @@
form-data "^3.0.0" form-data "^3.0.0"
"@types/node@*", "@types/node@>=10.0.0": "@types/node@*", "@types/node@>=10.0.0":
version "18.0.2" version "18.11.9"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.2.tgz#a594e580c396c22dd6b1470be81737c79ec0b1b1" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.9.tgz#02d013de7058cea16d36168ef2fc653464cfbad4"
integrity sha512-b947SdS4GH+g2W33wf5FzUu1KLj5FcSIiNWbU1ZyMvt/X7w48ZsVcsQoirIgE/Oq03WT5Qbn/dkY0hePi4ZXcQ== integrity sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==
"@types/node@^14.0.10 || ^16.0.0", "@types/node@^14.14.20 || ^16.0.0": "@types/node@^14.0.10 || ^16.0.0", "@types/node@^14.14.20 || ^16.0.0":
version "16.11.43" version "16.11.43"
@@ -8225,9 +8225,9 @@ engine.io-parser@~5.0.3:
integrity sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg== integrity sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==
engine.io@~6.2.0: engine.io@~6.2.0:
version "6.2.0" version "6.2.1"
resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.2.0.tgz#003bec48f6815926f2b1b17873e576acd54f41d0" resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.2.1.tgz#e3f7826ebc4140db9bbaa9021ad6b1efb175878f"
integrity sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg== integrity sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==
dependencies: dependencies:
"@types/cookie" "^0.4.1" "@types/cookie" "^0.4.1"
"@types/cors" "^2.8.12" "@types/cors" "^2.8.12"
@@ -15359,7 +15359,7 @@ react-final-form-arrays@^3.1.3:
dependencies: dependencies:
"@babel/runtime" "^7.12.1" "@babel/runtime" "^7.12.1"
react-final-form@^6.5.3: react-final-form@^6.5.9:
version "6.5.9" version "6.5.9"
resolved "https://registry.yarnpkg.com/react-final-form/-/react-final-form-6.5.9.tgz#644797d4c122801b37b58a76c87761547411190b" resolved "https://registry.yarnpkg.com/react-final-form/-/react-final-form-6.5.9.tgz#644797d4c122801b37b58a76c87761547411190b"
integrity sha512-x3XYvozolECp3nIjly+4QqxdjSSWfcnpGEL5K8OBT6xmGrq5kBqbA6+/tOqoom9NwqIPPbxPNsOViFlbKgowbA== integrity sha512-x3XYvozolECp3nIjly+4QqxdjSSWfcnpGEL5K8OBT6xmGrq5kBqbA6+/tOqoom9NwqIPPbxPNsOViFlbKgowbA==