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-dom": "^18.1.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-loader-spinner": "^4.0.0",
"react-masonry-css": "^1.0.16",

View File

@@ -32,6 +32,8 @@ import {
SS_SEARCH_FAILED,
SS_SEARCH_RESULTS_FETCHED_SPECIAL,
WANTED_COMICS_FETCHED,
VOLUMES_FETCHED,
CV_WEEKLY_PULLLIST_FETCHED,
} from "../constants/action-types";
import { success } from "react-notification-system-redux";
import { removeLeadingPeriod } from "../shared/utils/formatting.utils";
@@ -194,6 +196,7 @@ export const fetchVolumeGroups = () => async (dispatch) => {
};
export const fetchComicVineMatches =
(searchPayload, issueSearchQuery, seriesSearchQuery?) => async (dispatch) => {
console.log(issueSearchQuery);
try {
dispatch({
type: CV_API_CALL_IN_PROGRESS,
@@ -223,7 +226,6 @@ export const fetchComicVineMatches =
},
})
.then((response) => {
console.log(response);
let matches: any = [];
if (
!isNil(response.data.results) &&
@@ -233,7 +235,6 @@ export const fetchComicVineMatches =
} else {
matches = response.data.map((match) => match);
}
console.log(matches);
dispatch({
type: CV_SEARCH_SUCCESS,
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[] = [];
dispatch({
type: IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_IN_PROGRESS,
@@ -317,12 +324,16 @@ export const searchIssue = (query, options) => async (dispatch) => {
data: response.data.body,
});
break;
case "volumesPage":
dispatch({
type: VOLUMES_FETCHED,
data: response.data.body,
});
break;
default:
break;
}
};
export const analyzeImage =
(imageFilePath: string | Buffer) => async (dispatch) => {

View File

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

View File

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

View File

@@ -4,11 +4,25 @@ import { useDispatch } from "react-redux";
import { saveSettings, deleteSettings } from "../../actions/settings.actions";
import { AirDCPPSettingsConfirmation } from "./AirDCPPSettingsConfirmation";
import { AirDCPPSocketContext } from "../../context/AirDCPPSocket";
import { isUndefined, isEmpty } from "lodash";
import { isUndefined, isEmpty, isNil } from "lodash";
export const AirDCPPSettingsForm = (): ReactElement => {
const dispatch = useDispatch();
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) => {
try {
airDCPPSettings.setSettings(values);
@@ -25,7 +39,7 @@ export const AirDCPPSettingsForm = (): ReactElement => {
airDCPPSettings.setSettings({});
dispatch(deleteSettings());
}, []);
const validate = async () => {};
const validate = async () => { };
const initFormData = !isUndefined(
airDCPPSettings.airDCPPState.settings.directConnect,
)
@@ -52,12 +66,24 @@ export const AirDCPPSettingsForm = (): ReactElement => {
</Field>
</span>
</p>
<p className="control is-expanded">
<div className="control is-expanded">
<Field
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"
className="input"
placeholder="AirDC++ host IP / hostname"
placeholder="AirDC++ port"
/>
</p>
</div>

View File

@@ -3,14 +3,11 @@ import Dashboard from "./Dashboard/Dashboard";
import Import from "./Import";
import { ComicDetailContainer } from "./ComicDetail/ComicDetailContainer";
import LibraryContainer from "./Library/LibraryContainer";
import TabulatedContentContainer from "./Library/TabulatedContentContainer";
import LibraryGrid from "./Library/LibraryGrid";
import Search from "./Search";
import Settings from "./Settings";
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 { Routes, Route } from "react-router-dom";
@@ -23,15 +20,18 @@ import {
import { isEmpty, isUndefined } from "lodash";
import { AIRDCPP_DOWNLOAD_PROGRESS_TICK } from "../constants/action-types";
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 airDCPPConfiguration = useContext(AirDCPPSocketContext);
const dispatch = useDispatch();
useEffect(() => {
const foo = async () => {
const initializeAirDCPPEventListeners = async () => {
if (
!isUndefined(airDCPPConfiguration.airDCPPState) &&
!isEmpty(airDCPPConfiguration.airDCPPState.settings) &&
@@ -79,7 +79,7 @@ const AirDCPPSocketComponent = (): ReactElement => {
);
}
};
foo();
initializeAirDCPPEventListeners();
}, [airDCPPConfiguration]);
return <></>;
};
@@ -92,7 +92,7 @@ export const App = (): ReactElement => {
<Routes>
<Route path="/" element={<Dashboard />} />
<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="/downloads" element={<Downloads data={{}} />} />
<Route path="/search" element={<Search />} />
@@ -105,9 +105,9 @@ export const App = (): ReactElement => {
element={<VolumeDetail />}
/>
<Route path="/settings" element={<Settings />} />
<Route path="/pull-list/all" element={<PullList />} />
<Route path="/wanted/all" element={<WantedComics />} />
<Route path="/volumes/all" element={<Volumes />} />
<Route path="/pull-list/all" element={<TabulatedContentContainer category="pullList" />} />
<Route path="/wanted/all" element={<TabulatedContentContainer category="wanted" />} />
<Route path="/volumes/all" element={<TabulatedContentContainer category="volumes" />} />
</Routes>
</div>
</AirDCPPSocketContextProvider>

View File

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

View File

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

View File

@@ -10,38 +10,8 @@ export const ComicVineMatchPanel = (comicVineData): ReactElement => {
comicVineAPICallProgress,
comicVineSearchResults,
} = comicVineData.props;
console.log(comicVineData);
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">
{!isEmpty(comicVineSearchResults) && (
<MatchResult

View File

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

View File

@@ -20,7 +20,7 @@ export const PullList = ({ issues }: PullListProps): ReactElement => {
useEffect(() => {
dispatch(
getWeeklyPullList({
startDate: "2022-8-9",
startDate: "2022-11-15",
pageSize: "15",
currentPage: "1",
}),
@@ -127,7 +127,7 @@ export const PullList = ({ issues }: PullListProps): ReactElement => {
<Slider {...settings} ref={(c) => (sliderRef = c)}>
{!isNil(pullList) &&
pullList &&
map(pullList, (issue, idx) => {
map(pullList, ({issue}, idx) => {
return (
<Card
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 { useNavigate } from "react-router-dom";
import T2Table from "../shared/T2Table";
import { isEmpty, isNil, isUndefined } from "lodash";
import MetadataPanel from "../shared/MetadataPanel";
import SearchBar from "./SearchBar";
import { useDispatch } from "react-redux";
import T2Table from "../shared/T2Table";
import { useDispatch, useSelector } from "react-redux";
import { searchIssue } from "../../actions/fileops.actions";
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
const navigate = useNavigate();
@@ -65,69 +89,73 @@ export const Library = (data: IComicBookLibraryProps): ReactElement => {
const WantedStatus = ({ value }) => {
return !value ? <span className="tag is-info is-light">Wanted</span> : null;
};
const columns = [
{
header: "Comic Metadata",
footer: 1,
columns: [
{
header: "File Details",
id: "fileDetails",
minWidth: 400,
accessorKey: "_source",
cell: info => {
return <MetadataPanel data={info.getValue()} />;
},
const columns = useMemo(() => [
{
header: "Comic Metadata",
footer: 1,
columns: [
{
header: "File Details",
id: "fileDetails",
minWidth: 400,
accessorKey: "_source",
cell: info => {
return <MetadataPanel data={info.getValue()} />;
},
{
header: "ComicInfo.xml",
accessorKey: "_source.sourcedMetadata.comicInfo",
align: "center",
minWidth: 250,
cell: info =>
!isEmpty(info.getValue()) ? (
<ComicInfoXML data={info.getValue()} />
) : (
<span className="tag">No ComicInfo.xml</span>
),
},
{
header: "ComicInfo.xml",
accessorKey: "_source.sourcedMetadata.comicInfo",
align: "center",
minWidth: 250,
cell: info =>
!isEmpty(info.getValue()) ? (
<ComicInfoXML data={info.getValue()} />
) : (
<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: "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",
accessorKey: "_source.acquisition.source.wanted",
cell: info => {
!isUndefined(info.getValue()) ?
},
{
header: "Something",
accessorKey: "_source.acquisition.source.wanted",
cell: info => {
!isUndefined(info.getValue()) ?
<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(
searchIssue(
{
@@ -139,17 +167,26 @@ export const Library = (data: IComicBookLibraryProps): ReactElement => {
from: pageSize * pageIndex + 1,
},
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;
if (pageIndex === 2) {
from = (pageIndex - 1) * pageSize + 2 - 27;
from = (pageIndex - 1) * pageSize + 2 - 17;
} else {
from = (pageIndex - 1) * pageSize + 2 - 26;
from = (pageIndex - 1) * pageSize + 2 - 16;
}
dispatch(
searchIssue(
@@ -162,30 +199,35 @@ export const Library = (data: IComicBookLibraryProps): ReactElement => {
from,
},
type: "all",
trigger: "libraryPage"
},
),
);
}, []);
// ImportStatus.propTypes = {
// value: PropTypes.bool.isRequired,
// };
return (
<section className="container">
<div className="section">
<h1 className="title">Library</h1>
{/* Search bar */}
<SearchBar />
{!isUndefined(searchResults) && (
<div className="header-area">
<h1 className="title">Library</h1>
</div>
{!isUndefined(searchResults.hits) && (
<div>
<div className="library">
<T2Table
rowData={searchResults.hits.hits}
totalPages={searchResults.hits.total.value}
columns={columns}
paginationHandlers={{
nextPage: goToNextPage,
previousPage: goToPreviousPage,
}}
sourceData={searchResults?.hits?.hits}
rowClickHandler={navigateToComicDetail}
paginationHandlers={{
nextPage,
previousPage,
}}
/>
{/* pagination controls */}
</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,
},
type: "volumeName",
trigger: "libraryPage",
},
),
);
}, []);
return (
<div className="box sticky">
<div className="box">
<Form
onSubmit={handleSubmit}
initialValues={{}}
render={({ handleSubmit, form, submitting, pristine, values }) => (
<form onSubmit={handleSubmit}>
<div className="field is-grouped">
<div className="control is-expanded search">
<div className="control search is-expanded">
<Field name="search">
{({ input, meta }) => {
return (
@@ -55,26 +56,7 @@ export const SearchBar = (): ReactElement => {
</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>
);
};

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

View File

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

View File

@@ -77,7 +77,9 @@ export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => {
</div>
)}
</div>
</div>
</dd>
</dl>
),
@@ -211,7 +213,7 @@ export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => {
orientation={"vertical"}
hasDetails={false}
imageStyle={props.imageStyle}
// cardContainerStyle={{ maxWidth: 200 }}
// cardContainerStyle={{ maxWidth: 200 }}
/>
</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 SearchBar from "../Library/SearchBar";
import { Link } from "react-router-dom";
import {
ColumnDef,
flexRender,
getCoreRowModel,
getFilteredRowModel,
useReactTable,
PaginationState,
} from "@tanstack/react-table";
export const T2Table = (tableOptions): ReactElement => {
const { rowData, columns, paginationHandlers, totalPages, rowClickHandler } =
const { sourceData, columns, paginationHandlers: { nextPage, previousPage }, totalPages, rowClickHandler } =
tableOptions;
const [isPageSizeDropdownCollapsed, collapsePageSizeDropdown] =
useState(false);
const togglePageSizeDropdown = () =>
collapsePageSizeDropdown(!isPageSizeDropdownCollapsed);
const [{ pageIndex, pageSize }, setPagination] =
useState<PaginationState>({
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({
data: rowData,
data: sourceData,
columns,
manualPagination: true,
getCoreRowModel: getCoreRowModel(),
pageCount: sourceData.length ?? -1,
state: {
pagination,
},
onPaginationChange: setPagination,
});
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">
<thead>
{table.getHeaderGroups().map((headerGroup, idx) => (
@@ -60,15 +147,12 @@ export const T2Table = (tableOptions): ReactElement => {
})}
</tbody>
</table>
{/* pagination control */}
</>
);
};
T2Table.propTypes = {
rowData: PropTypes.array,
sourceData: PropTypes.array,
totalPages: PropTypes.number,
columns: PropTypes.array,
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";
export const IMS_COMIC_BOOK_GROUPS_CALL_FAILED =
"IMS_COMIC_BOOK_GROUPS_CALL_FAILED";
export const VOLUMES_FETCHED="VOLUMES_FETCHED";
// search results from the Search service
export const SS_SEARCH_RESULTS_FETCHED = "SS_SEARCH_RESULTS_FETCHED";

View File

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

View File

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

View File

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

View File

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