🦟 Fixed 404s upon page refresh

This commit is contained in:
2022-03-01 23:01:57 -08:00
parent 769e2e3edc
commit 9ec5040bd7
10 changed files with 238 additions and 189 deletions

View File

@@ -5,6 +5,7 @@ import {
IMAGETRANSFORMATION_SERVICE_BASE_URI, IMAGETRANSFORMATION_SERVICE_BASE_URI,
LIBRARY_SERVICE_BASE_URI, LIBRARY_SERVICE_BASE_URI,
LIBRARY_SERVICE_HOST, LIBRARY_SERVICE_HOST,
SEARCH_SERVICE_BASE_URI,
} from "../constants/endpoints"; } from "../constants/endpoints";
import { import {
IMS_COMIC_BOOK_GROUPS_FETCHED, IMS_COMIC_BOOK_GROUPS_FETCHED,
@@ -257,7 +258,14 @@ export const extractComicArchive =
}); });
}; };
export const searchIssue = (options) => async (dispatch) => {}; export const searchIssue = (query) => async (dispatch) => {
const foo = await axios({
url: `${SEARCH_SERVICE_BASE_URI}/searchIssue`,
method: "POST",
data: query,
});
console.log(foo);
};
export const analyzeImage = export const analyzeImage =
(imageFilePath: string | Buffer) => async (dispatch) => { (imageFilePath: string | Buffer) => async (dispatch) => {
dispatch({ dispatch({

View File

@@ -12,7 +12,7 @@ import DownloadsPanel from "./ComicDetail/DownloadsPanel";
import { EditMetadataPanel } from "./ComicDetail/EditMetadataPanel"; import { EditMetadataPanel } from "./ComicDetail/EditMetadataPanel";
import { Menu } from "./ComicDetail/ActionMenu/Menu"; import { Menu } from "./ComicDetail/ActionMenu/Menu";
import { isEmpty, isUndefined, isNil, findIndex } from "lodash"; import { isEmpty, isUndefined, isNil } from "lodash";
import { RootState } from "threetwo-ui-typings"; import { RootState } from "threetwo-ui-typings";
import { getComicBookDetailById } from "../actions/comicinfo.actions"; import { getComicBookDetailById } from "../actions/comicinfo.actions";
@@ -117,6 +117,7 @@ export const ComicDetail = ({}: ComicDetailProps): ReactElement => {
}, },
}; };
// check for the availability of CV metadata
const isComicBookMetadataAvailable = const isComicBookMetadataAvailable =
comicBookDetailData.sourcedMetadata && comicBookDetailData.sourcedMetadata &&
!isUndefined(comicBookDetailData.sourcedMetadata.comicvine) && !isUndefined(comicBookDetailData.sourcedMetadata.comicvine) &&
@@ -125,6 +126,29 @@ export const ComicDetail = ({}: ComicDetailProps): ReactElement => {
) && ) &&
!isEmpty(comicBookDetailData.sourcedMetadata); !isEmpty(comicBookDetailData.sourcedMetadata);
// check for the availability of rawFileDetails
const areRawFileDetailsAvailable =
!isUndefined(comicBookDetailData.rawFileDetails) &&
!isEmpty(comicBookDetailData.rawFileDetails.cover);
// query for airdc++
const airDCPPQuery = {};
if (isComicBookMetadataAvailable) {
Object.assign(airDCPPQuery, {
issue: {
name: comicBookDetailData.sourcedMetadata.comicvine.volumeInformation
.name,
},
});
} else if (areRawFileDetailsAvailable) {
Object.assign(airDCPPQuery, {
issue: {
name: comicBookDetailData.inferredMetadata.issue.name,
number: comicBookDetailData.inferredMetadata.issue.number,
},
});
}
// Tab content and header details // Tab content and header details
const tabGroup = [ const tabGroup = [
{ {
@@ -134,7 +158,7 @@ export const ComicDetail = ({}: ComicDetailProps): ReactElement => {
content: isComicBookMetadataAvailable ? ( content: isComicBookMetadataAvailable ? (
<VolumeInformation data={comicBookDetailData} key={1} /> <VolumeInformation data={comicBookDetailData} key={1} />
) : null, ) : null,
include: isComicBookMetadataAvailable, shouldShow: isComicBookMetadataAvailable,
}, },
{ {
id: 2, id: 2,
@@ -156,8 +180,8 @@ export const ComicDetail = ({}: ComicDetailProps): ReactElement => {
</div> </div>
</div> </div>
), ),
include: shouldShow:
!isNil(comicBookDetailData.sourcedMetadata) && !isUndefined(comicBookDetailData.sourcedMetadata) &&
!isEmpty(comicBookDetailData.sourcedMetadata.comicInfo), !isEmpty(comicBookDetailData.sourcedMetadata.comicInfo),
}, },
{ {
@@ -165,18 +189,14 @@ export const ComicDetail = ({}: ComicDetailProps): ReactElement => {
icon: <i className="fa-regular fa-file-archive"></i>, icon: <i className="fa-regular fa-file-archive"></i>,
name: "Archive Operations", name: "Archive Operations",
content: <ArchiveOperations data={comicBookDetailData} key={3} />, content: <ArchiveOperations data={comicBookDetailData} key={3} />,
include: shouldShow: areRawFileDetailsAvailable,
!isUndefined(comicBookDetailData.rawFileDetails) &&
!isEmpty(comicBookDetailData.rawFileDetails.cover),
}, },
{ {
id: 4, id: 4,
icon: <i className="fa-solid fa-floppy-disk"></i>, icon: <i className="fa-solid fa-floppy-disk"></i>,
name: "Acquisition", name: "Acquisition",
content: ( content: <AcquisitionPanel query={airDCPPQuery} key={4} />,
<AcquisitionPanel comicBookMetadata={comicBookDetailData} key={4} /> shouldShow: true,
),
include: !isNil(comicBookDetailData.rawFileDetails),
}, },
{ {
id: 5, id: 5,
@@ -194,12 +214,11 @@ export const ComicDetail = ({}: ComicDetailProps): ReactElement => {
key={5} key={5}
/> />
), ),
include: !isNil(comicBookDetailData.rawFileDetails), shouldShow: true,
}, },
]; ];
// filtered Tabs // filtered Tabs
const filteredTabs = tabGroup.filter((tab) => tab.include); const filteredTabs = tabGroup.filter((tab) => tab.shouldShow);
// Tabs // Tabs
const MetadataTabGroup = () => { const MetadataTabGroup = () => {
@@ -245,10 +264,7 @@ export const ComicDetail = ({}: ComicDetailProps): ReactElement => {
// 2. from the CV-scraped version // 2. from the CV-scraped version
let imagePath = ""; let imagePath = "";
let comicBookTitle = ""; let comicBookTitle = "";
if ( if (areRawFileDetailsAvailable) {
!isUndefined(comicBookDetailData.rawFileDetails) &&
!isEmpty(comicBookDetailData.rawFileDetails.cover)
) {
const encodedFilePath = encodeURI( const encodedFilePath = encodeURI(
`${LIBRARY_SERVICE_HOST}/${comicBookDetailData.rawFileDetails.cover.filePath}`, `${LIBRARY_SERVICE_HOST}/${comicBookDetailData.rawFileDetails.cover.filePath}`,
); );

View File

@@ -16,16 +16,15 @@ import ellipsize from "ellipsize";
import { isEmpty, isNil, map } from "lodash"; import { isEmpty, isNil, map } from "lodash";
import { AirDCPPSocketContext } from "../../context/AirDCPPSocket"; import { AirDCPPSocketContext } from "../../context/AirDCPPSocket";
interface IAcquisitionPanelProps { interface IAcquisitionPanelProps {
comicBookMetadata: any; query: any;
} }
export const AcquisitionPanel = ( export const AcquisitionPanel = (
props: IAcquisitionPanelProps, props: IAcquisitionPanelProps,
): ReactElement => { ): ReactElement => {
const volumeName = console.log(props);
props.comicBookMetadata.sourcedMetadata.comicvine.volumeInformation.name; const issueName = props.query.issue.name;
const sanitizedVolumeName = volumeName.replace(/[^a-zA-Z0-9 ]/g, " "); const sanitizedIssueName = issueName.replace(/[^a-zA-Z0-9 ]/g, " ");
const issueName = props.comicBookMetadata.sourcedMetadata.comicvine.name;
// Selectors for picking state // Selectors for picking state
const airDCPPSearchResults = useSelector((state: RootState) => { const airDCPPSearchResults = useSelector((state: RootState) => {
@@ -51,7 +50,7 @@ export const AcquisitionPanel = (
// AirDC++ search query // AirDC++ search query
const dcppSearchQuery = { const dcppSearchQuery = {
query: { query: {
pattern: `${sanitizedVolumeName.replace(/#/g, "")}`, pattern: `${sanitizedIssueName.replace(/#/g, "")}`,
extensions: ["cbz", "cbr"], extensions: ["cbz", "cbr"],
}, },
hub_urls: map( hub_urls: map(

View File

@@ -191,151 +191,154 @@ export const Library = ({}: IComicBookLibraryProps): ReactElement => {
<h1 className="title">Library</h1> <h1 className="title">Library</h1>
{/* Search bar */} {/* Search bar */}
<SearchBar /> <SearchBar />
<div> {!isUndefined(data) ? (
<div className="library"> <div>
<table {...getTableProps()} className="table is-hoverable"> <div className="library">
<thead> <table {...getTableProps()} className="table is-hoverable">
{headerGroups.map((headerGroup, idx) => ( <thead>
<tr key={idx} {...headerGroup.getHeaderGroupProps()}> {headerGroups.map((headerGroup, idx) => (
{headerGroup.headers.map((column, idx) => ( <tr key={idx} {...headerGroup.getHeaderGroupProps()}>
<th key={idx} {...column.getHeaderProps()}> {headerGroup.headers.map((column, idx) => (
{column.render("Header")} <th key={idx} {...column.getHeaderProps()}>
</th> {column.render("Header")}
))} </th>
</tr> ))}
))}
</thead>
<tbody {...getTableBodyProps()}>
{page.map((row, idx) => {
prepareRow(row);
return (
<tr
key={idx}
{...row.getRowProps()}
onClick={() => navigateToComicDetail(row.original._id)}
>
{row.cells.map((cell, idx) => {
return (
<td
key={idx}
{...cell.getCellProps()}
className="is-vcentered"
>
{cell.render("Cell")}
</td>
);
})}
</tr> </tr>
); ))}
})} </thead>
</tbody>
</table>
{/* pagination controls */} <tbody {...getTableBodyProps()}>
<nav {page.map((row, idx) => {
className="pagination" prepareRow(row);
role="navigation" return (
aria-label="pagination" <tr
> key={idx}
{/* x of total indicator */} {...row.getRowProps()}
<div> onClick={() => navigateToComicDetail(row.original._id)}
Page {pageIndex} of {Math.ceil(pageTotal / pageSize)} >
(Total resources: {pageTotal}) {row.cells.map((cell, idx) => {
</div> return (
<td
key={idx}
{...cell.getCellProps()}
className="is-vcentered"
>
{cell.render("Cell")}
</td>
);
})}
</tr>
);
})}
</tbody>
</table>
{/* previous page and next page controls */} {/* pagination controls */}
<div className="field has-addons"> <nav
<p className="control"> className="pagination"
<button role="navigation"
className="button" aria-label="pagination"
onClick={() => previousPage()}
disabled={!canPreviousPage}
>
Previous Page
</button>
</p>
<p className="control">
<button
className="button"
onClick={() => nextPage()}
disabled={!canNextPage}
>
<span>Next Page</span>
</button>
</p>
</div>
{/* first and last page controls */}
<div className="field has-addons">
<p className="control">
<button
className="button"
onClick={() => gotoPage(1)}
disabled={!canPreviousPage}
>
<i className="fas fa-angle-double-left"></i>
</button>
</p>
<p className="control">
<button
className="button"
onClick={() => gotoPage(Math.ceil(pageTotal / pageSize))}
disabled={!canNextPage}
>
<i className="fas fa-angle-double-right"></i>
</button>
</p>
</div>
{/* page selector */}
<span>
Go to page:
<input
type="number"
className="input"
defaultValue={pageIndex}
onChange={(e) => {
const page = e.target.value ? Number(e.target.value) : 0;
gotoPage(page);
}}
style={{ width: "100px" }}
/>
</span>
{/* page size selector */}
<div
className={
"dropdown " + (isPageSizeDropdownCollapsed ? "is-active" : "")
}
onBlur={() => togglePageSizeDropdown()}
> >
<div className="dropdown-trigger"> {/* x of total indicator */}
<button <div>
className="button" Page {pageIndex} of {Math.ceil(pageTotal / pageSize)}
aria-haspopup="true" (Total resources: {pageTotal})
aria-controls="dropdown-menu"
onClick={() => togglePageSizeDropdown()}
>
<span>Select Page Size</span>
<span className="icon is-small">
<i className="fas fa-angle-down" aria-hidden="true"></i>
</span>
</button>
</div> </div>
<div className="dropdown-menu" id="dropdown-menu" role="menu">
<div className="dropdown-content"> {/* previous page and next page controls */}
{[10, 20, 30, 40, 50].map((pageSize) => ( <div className="field has-addons">
<a href="#" className="dropdown-item" key={pageSize}> <p className="control">
Show {pageSize} <button
</a> className="button"
))} onClick={() => previousPage()}
disabled={!canPreviousPage}
>
Previous Page
</button>
</p>
<p className="control">
<button
className="button"
onClick={() => nextPage()}
disabled={!canNextPage}
>
<span>Next Page</span>
</button>
</p>
</div>
{/* first and last page controls */}
<div className="field has-addons">
<p className="control">
<button
className="button"
onClick={() => gotoPage(1)}
disabled={!canPreviousPage}
>
<i className="fas fa-angle-double-left"></i>
</button>
</p>
<p className="control">
<button
className="button"
onClick={() => gotoPage(Math.ceil(pageTotal / pageSize))}
disabled={!canNextPage}
>
<i className="fas fa-angle-double-right"></i>
</button>
</p>
</div>
{/* page selector */}
<span>
Go to page:
<input
type="number"
className="input"
defaultValue={pageIndex}
onChange={(e) => {
const page = e.target.value ? Number(e.target.value) : 0;
gotoPage(page);
}}
style={{ width: "100px" }}
/>
</span>
{/* page size selector */}
<div
className={
"dropdown " +
(isPageSizeDropdownCollapsed ? "is-active" : "")
}
onBlur={() => togglePageSizeDropdown()}
>
<div className="dropdown-trigger">
<button
className="button"
aria-haspopup="true"
aria-controls="dropdown-menu"
onClick={() => togglePageSizeDropdown()}
>
<span>Select Page Size</span>
<span className="icon is-small">
<i className="fas fa-angle-down" aria-hidden="true"></i>
</span>
</button>
</div>
<div className="dropdown-menu" id="dropdown-menu" role="menu">
<div className="dropdown-content">
{[10, 20, 30, 40, 50].map((pageSize) => (
<a href="#" className="dropdown-item" key={pageSize}>
Show {pageSize}
</a>
))}
</div>
</div> </div>
</div> </div>
</div> </nav>
</nav> </div>
</div> </div>
</div> ) : null}
</div> </div>
</section> </section>
); );

View File

@@ -1,30 +1,47 @@
import React, { ReactElement } from "react"; import React, { ReactElement, useCallback } from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { Form, Field } from "react-final-form"; import { Form, Field } from "react-final-form";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { useDispatch } from "react-redux";
import { searchIssue } from "../../actions/fileops.actions";
export const SearchBar = (): ReactElement => { export const SearchBar = (): ReactElement => {
const foo = () => {}; const dispatch = useDispatch();
const handleSubmit = useCallback((e) => {
console.log(e);
dispatch(
searchIssue({
queryObject: {
volumeName: e.search,
},
}),
);
}, []);
return ( return (
<div className="box sticky"> <div className="box sticky">
<Form <Form
onSubmit={foo} onSubmit={handleSubmit}
initialValues={{}} initialValues={{}}
render={({ handleSubmit, form, submitting, pristine, values }) => ( render={({ handleSubmit, form, submitting, pristine, values }) => (
<div className="column is-three-quarters search"> <form onSubmit={handleSubmit}>
<label>Search</label> <div className="column is-three-quarters search">
<Field name="search"> <label>Search</label>
{({ input, meta }) => { <Field name="search">
return ( {({ input, meta }) => {
<input return (
{...input} <input
className="input main-search-bar is-medium" {...input}
placeholder="Type an issue/volume name" className="input main-search-bar is-medium"
/> placeholder="Type an issue/volume name"
); />
}} );
</Field> }}
</div> </Field>
<button className="button" type="submit">
Search
</button>
</div>
</form>
)} )}
/> />
<div className="column one-fifth"> <div className="column one-fifth">

View File

@@ -1,6 +1,6 @@
import React, { useState, useEffect, useMemo, ReactElement } from "react"; import React, { useState, useEffect, useMemo, ReactElement } from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { useHistory } from "react-router"; import { useNavigate } from "react-router";
import { import {
removeLeadingPeriod, removeLeadingPeriod,
escapePoundSymbol, escapePoundSymbol,
@@ -10,7 +10,7 @@ import prettyBytes from "pretty-bytes";
import ellipsize from "ellipsize"; import ellipsize from "ellipsize";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { getComicBooks } from "../actions/fileops.actions"; import { getComicBooks } from "../actions/fileops.actions";
import { isNil, isEmpty } from "lodash"; import { isNil, isEmpty, isUndefined } from "lodash";
import Masonry from "react-masonry-css"; import Masonry from "react-masonry-css";
import Card from "./Carda"; import Card from "./Carda";
import { detectIssueTypes } from "../shared/utils/tradepaperback.utils"; import { detectIssueTypes } from "../shared/utils/tradepaperback.utils";
@@ -45,7 +45,7 @@ export const LibraryGrid = (libraryGridProps: ILibraryGridProps) => {
{data.map(({ _id, rawFileDetails, sourcedMetadata }) => { {data.map(({ _id, rawFileDetails, sourcedMetadata }) => {
let imagePath = ""; let imagePath = "";
let comicName = ""; let comicName = "";
if (!isNil(rawFileDetails)) { if (!isEmpty(rawFileDetails.cover)) {
const encodedFilePath = encodeURI( const encodedFilePath = encodeURI(
`${LIBRARY_SERVICE_HOST}/${removeLeadingPeriod( `${LIBRARY_SERVICE_HOST}/${removeLeadingPeriod(
rawFileDetails.cover.filePath, rawFileDetails.cover.filePath,
@@ -71,7 +71,7 @@ export const LibraryGrid = (libraryGridProps: ILibraryGridProps) => {
title={comicName ? titleElement : null} title={comicName ? titleElement : null}
> >
<div className="content is-flex is-flex-direction-row"> <div className="content is-flex is-flex-direction-row">
{!isNil(sourcedMetadata.comicvine) && ( {!isEmpty(sourcedMetadata.comicvine) && (
<span className="icon cv-icon is-small"> <span className="icon cv-icon is-small">
<img src="/dist/img/cvlogo.svg" /> <img src="/dist/img/cvlogo.svg" />
</span> </span>
@@ -81,13 +81,13 @@ export const LibraryGrid = (libraryGridProps: ILibraryGridProps) => {
<i className="fas fa-adjust" /> <i className="fas fa-adjust" />
</span> </span>
)} )}
{!isNil(sourcedMetadata.comicvine) && {!isUndefined(sourcedMetadata.comicvine.volumeInformation) &&
!isEmpty( !isEmpty(
detectIssueTypes( detectIssueTypes(
sourcedMetadata.comicvine.volumeInformation.description, sourcedMetadata.comicvine.volumeInformation.description,
), ),
) ? ( ) ? (
<span className="tag is-warning"> <span className="tag is-warning ml-1">
{ {
detectIssueTypes( detectIssueTypes(
sourcedMetadata.comicvine.volumeInformation sourcedMetadata.comicvine.volumeInformation

View File

@@ -161,7 +161,7 @@ const VolumeDetails = (props): ReactElement => {
if ( if (
!isUndefined(comicBookDetails.sourcedMetadata) && !isUndefined(comicBookDetails.sourcedMetadata) &&
!isUndefined(comicBookDetails.sourcedMetadata.comicvine) !isUndefined(comicBookDetails.sourcedMetadata.comicvine.volumeInformation)
) { ) {
return ( return (
<div className="container volume-details"> <div className="container volume-details">

View File

@@ -49,6 +49,12 @@ export const LIBRARY_SERVICE_BASE_URI = hostURIBuilder({
port: "3000", port: "3000",
apiPath: "/api/library", apiPath: "/api/library",
}); });
export const SEARCH_SERVICE_BASE_URI = hostURIBuilder({
protocol: "http",
host: process.env.UNDERLYING_HOSTNAME || "localhost",
port: "3000",
apiPath: "/api/search",
});
export const SETTINGS_SERVICE_BASE_URI = hostURIBuilder({ export const SETTINGS_SERVICE_BASE_URI = hostURIBuilder({
protocol: "http", protocol: "http",

View File

@@ -1,5 +1,5 @@
import { createStore, combineReducers, applyMiddleware } from "redux"; import { createStore, combineReducers, applyMiddleware } from "redux";
import { createBrowserHistory } from "history"; import { createHashHistory } from "history";
import { composeWithDevTools } from "redux-devtools-extension"; import { composeWithDevTools } from "redux-devtools-extension";
import thunk from "redux-thunk"; import thunk from "redux-thunk";
import { createReduxHistoryContext } from "redux-first-history"; import { createReduxHistoryContext } from "redux-first-history";
@@ -12,7 +12,7 @@ const socketConnection = io(SOCKET_BASE_URI, { transports: ["websocket"] });
const { createReduxHistory, routerMiddleware, routerReducer } = const { createReduxHistory, routerMiddleware, routerReducer } =
createReduxHistoryContext({ createReduxHistoryContext({
history: createBrowserHistory(), history: createHashHistory(),
}); });
export const store = createStore( export const store = createStore(

View File

@@ -73,9 +73,9 @@ module.exports = () => {
aliasFields: ["browser", "browser.esm"], aliasFields: ["browser", "browser.esm"],
}, },
devServer: { devServer: {
hot: true,
port: 3050, port: 3050,
open: true, open: true,
hot: true,
proxy: { proxy: {
"/api/**": { "/api/**": {
target: "http://localhost:8050", target: "http://localhost:8050",