Compare commits
25 Commits
0.0.1-beta
...
rishighan-
| Author | SHA1 | Date | |
|---|---|---|---|
| 540c711904 | |||
| 0e73f9dad1 | |||
| a15168a6be | |||
|
|
67079f0cb4 | ||
| ef5af01e33 | |||
| 5b6c9c8ffc | |||
| 4556778a47 | |||
| af5f443cbe | |||
| 7babf9f73d | |||
| 1e39daeda2 | |||
| e3cea24615 | |||
| f60c9e4e67 | |||
| 27e6f26331 | |||
| 1f5502ce23 | |||
| 3cb9588bbf | |||
| b1fb256189 | |||
| 74ea2742f0 | |||
| 151c6ec314 | |||
| c6a3be968a | |||
| ff5ce10e17 | |||
| 63e96bf96e | |||
| b699a90a00 | |||
| eda11d3537 | |||
| 262f8d49d7 | |||
| 6b1bb02d57 |
BIN
ComicVine Matching.png
Normal file
BIN
ComicVine Matching.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
BIN
DC++ integration.png
Normal file
BIN
DC++ integration.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1013 KiB |
BIN
Dashboard.png
Normal file
BIN
Dashboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 MiB |
BIN
Library.png
Normal file
BIN
Library.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
@@ -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",
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -16,7 +16,6 @@ export const saveSettings =
|
||||
method: "POST",
|
||||
data: { settingsPayload, settingsObjectId },
|
||||
});
|
||||
console.log(result.data);
|
||||
dispatch({
|
||||
type: SETTINGS_OBJECT_FETCHED,
|
||||
data: result.data,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
66
src/client/components/Library/TabulatedContentContainer.tsx
Normal file
66
src/client/components/Library/TabulatedContentContainer.tsx
Normal 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;
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
14
yarn.lock
14
yarn.lock
@@ -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==
|
||||
|
||||
Reference in New Issue
Block a user