🔍 Refining CV search UX
This commit is contained in:
@@ -18,6 +18,8 @@
|
||||
"@dnd-kit/core": "^6.0.8",
|
||||
"@dnd-kit/sortable": "^7.0.2",
|
||||
"@dnd-kit/utilities": "^3.2.1",
|
||||
"@floating-ui/react": "^0.26.12",
|
||||
"@floating-ui/react-dom": "^2.0.8",
|
||||
"@fortawesome/fontawesome-free": "^6.3.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||
@@ -58,7 +60,6 @@
|
||||
"react-final-form-arrays": "^3.1.4",
|
||||
"react-loader-spinner": "^4.0.0",
|
||||
"react-modal": "^3.15.1",
|
||||
"react-popper": "^2.3.0",
|
||||
"react-router": "^6.9.0",
|
||||
"react-router-dom": "^6.9.0",
|
||||
"react-select": "^5.8.0",
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React, { useCallback, ReactElement, useState } from "react";
|
||||
import { isNil, isEmpty } from "lodash";
|
||||
import React, { ReactElement, useState } from "react";
|
||||
import { isNil, isEmpty, isUndefined } from "lodash";
|
||||
import { IExtractedComicBookCoverFile, RootState } from "threetwo-ui-typings";
|
||||
|
||||
import { detectIssueTypes } from "../../shared/utils/tradepaperback.utils";
|
||||
import { Form, Field } from "react-final-form";
|
||||
import Card from "../shared/Carda";
|
||||
import ellipsize from "ellipsize";
|
||||
import { convert } from "html-to-text";
|
||||
import PopoverButton from "../shared/PopoverButton";
|
||||
import dayjs from "dayjs";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
COMICVINE_SERVICE_URI,
|
||||
LIBRARY_SERVICE_BASE_URI,
|
||||
@@ -20,9 +21,12 @@ export const Search = ({}: ISearchProps): ReactElement => {
|
||||
const formData = {
|
||||
search: "",
|
||||
};
|
||||
const queryClient = useQueryClient();
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [comicVineMetadata, setComicVineMetadata] = useState({});
|
||||
const [selectedResource, setSelectedResource] = useState("volume");
|
||||
|
||||
const handleResourceChange = (value) => {
|
||||
setSelectedResource(value);
|
||||
};
|
||||
|
||||
const {
|
||||
mutate,
|
||||
@@ -43,7 +47,7 @@ export const Search = ({}: ISearchProps): ReactElement => {
|
||||
limit: "10",
|
||||
offset: "0",
|
||||
field_list:
|
||||
"id,name,deck,api_detail_url,image,description,volume,cover_date,count_of_issues",
|
||||
"id,name,deck,api_detail_url,image,description,volume,cover_date,start_year,count_of_issues",
|
||||
resources: resource,
|
||||
},
|
||||
});
|
||||
@@ -86,6 +90,17 @@ export const Search = ({}: ISearchProps): ReactElement => {
|
||||
return { __html: html };
|
||||
};
|
||||
|
||||
const onSubmit = async (values) => {
|
||||
// Include the selected resource value in the form data
|
||||
const formData = { ...values, resource: selectedResource };
|
||||
try {
|
||||
mutate(formData);
|
||||
// Handle response
|
||||
} catch (error) {
|
||||
// Handle error
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<section>
|
||||
@@ -106,7 +121,7 @@ export const Search = ({}: ISearchProps): ReactElement => {
|
||||
</header>
|
||||
<div className="mx-auto max-w-screen-sm px-4 py-4 sm:px-6 sm:py-8 lg:px-8">
|
||||
<Form
|
||||
onSubmit={mutate}
|
||||
onSubmit={onSubmit}
|
||||
initialValues={{
|
||||
...formData,
|
||||
}}
|
||||
@@ -148,6 +163,8 @@ export const Search = ({}: ISearchProps): ReactElement => {
|
||||
{...volumesInput}
|
||||
type="radio"
|
||||
id="volume"
|
||||
checked={selectedResource === "volume"}
|
||||
onChange={() => handleResourceChange("volume")}
|
||||
className="peer hidden"
|
||||
/>
|
||||
<label
|
||||
@@ -169,6 +186,8 @@ export const Search = ({}: ISearchProps): ReactElement => {
|
||||
{...issuesInput}
|
||||
type="radio"
|
||||
id="issue"
|
||||
checked={selectedResource === "issue"}
|
||||
onChange={() => handleResourceChange("issue")}
|
||||
className="peer hidden"
|
||||
/>
|
||||
<label
|
||||
@@ -186,14 +205,21 @@ export const Search = ({}: ISearchProps): ReactElement => {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{isPending && <>Loading results...</>}
|
||||
{isPending && (
|
||||
<div className="max-w-screen-xl px-4 py-4 sm:px-6 sm:py-8 lg:px-8">
|
||||
Loading results...
|
||||
</div>
|
||||
)}
|
||||
{!isEmpty(comicVineSearchResults?.data?.results) ? (
|
||||
<div className="mx-auto max-w-screen-xl px-4 py-4 sm:px-6 sm:py-8 lg:px-8">
|
||||
{comicVineSearchResults.data.results.map((result) => {
|
||||
return result.resource_type === "issue" ? (
|
||||
<div key={result.id} className="mb-5">
|
||||
<div
|
||||
key={result.id}
|
||||
className="mb-5 dark:bg-slate-400 p-4 rounded-lg"
|
||||
>
|
||||
<div className="flex flex-row">
|
||||
<div className="mr-5 min-w-[200px] max-w-[25%]">
|
||||
<div className="mr-5 min-w-[80px] max-w-[13%]">
|
||||
<Card
|
||||
key={result.id}
|
||||
orientation={"cover-only"}
|
||||
@@ -221,7 +247,7 @@ export const Search = ({}: ISearchProps): ReactElement => {
|
||||
<a href={result.api_detail_url}>
|
||||
{result.api_detail_url}
|
||||
</a>
|
||||
<p>
|
||||
<p className="text-sm">
|
||||
{ellipsize(
|
||||
convert(result.description, {
|
||||
baseElements: {
|
||||
@@ -245,9 +271,12 @@ export const Search = ({}: ISearchProps): ReactElement => {
|
||||
</div>
|
||||
) : (
|
||||
result.resource_type === "volume" && (
|
||||
<div key={result.id} className="mb-5">
|
||||
<div
|
||||
key={result.id}
|
||||
className="mb-5 dark:bg-slate-500 p-4 rounded-lg"
|
||||
>
|
||||
<div className="flex flex-row">
|
||||
<div className="mr-5">
|
||||
<div className="mr-5 min-w-[80px] max-w-[13%]">
|
||||
<Card
|
||||
key={result.id}
|
||||
orientation={"cover-only"}
|
||||
@@ -255,39 +284,63 @@ export const Search = ({}: ISearchProps): ReactElement => {
|
||||
hasDetails={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="column">
|
||||
<div className="w-3/4">
|
||||
<div className="text-xl">
|
||||
{!isEmpty(result.name) ? (
|
||||
result.name
|
||||
) : (
|
||||
<span className="is-size-3">No Name</span>
|
||||
<span className="text-xl">No Name</span>
|
||||
)}
|
||||
{result.start_year && <> ({result.start_year})</>}
|
||||
</div>
|
||||
<div className="field is-grouped mt-1">
|
||||
<div className="control">
|
||||
<div className="tags has-addons">
|
||||
<span className="tag is-light">
|
||||
Number of issues
|
||||
<div className="flex flex-row gap-2">
|
||||
{/* issue count */}
|
||||
<div className="my-2">
|
||||
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||
<span className="pr-1 pt-1">
|
||||
<i className="icon-[solar--documents-minimalistic-bold-duotone] w-5 h-5"></i>
|
||||
</span>
|
||||
<span className="tag is-info is-light">
|
||||
{result.count_of_issues}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="control">
|
||||
<div className="tags has-addons">
|
||||
<span className="tag is-warning">
|
||||
{result.id}
|
||||
<span className="text-md text-slate-500 dark:text-slate-900">
|
||||
{result.count_of_issues} issues
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
{/* type: TPB, one-shot, graphic novel etc. */}
|
||||
{!isNil(result.description) &&
|
||||
!isUndefined(result.description) && (
|
||||
<>
|
||||
{!isEmpty(
|
||||
detectIssueTypes(result.description),
|
||||
) && (
|
||||
<div className="my-2">
|
||||
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||
<span className="pr-1 pt-1">
|
||||
<i className="icon-[solar--book-2-line-duotone] w-5 h-5"></i>
|
||||
</span>
|
||||
|
||||
<span className="text-md text-slate-500 dark:text-slate-900">
|
||||
{
|
||||
detectIssueTypes(result.description)
|
||||
.displayName
|
||||
}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<a href={result.api_detail_url}>
|
||||
{result.api_detail_url}
|
||||
</a>
|
||||
<span className="tag is-warning">{result.id}</span>
|
||||
<p>
|
||||
<a href={result.api_detail_url}>
|
||||
{result.api_detail_url}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
{/* description */}
|
||||
<p className="text-sm">
|
||||
{ellipsize(
|
||||
convert(result.description, {
|
||||
baseElements: {
|
||||
@@ -298,13 +351,8 @@ export const Search = ({}: ISearchProps): ReactElement => {
|
||||
)}
|
||||
</p>
|
||||
<div className="mt-2">
|
||||
<button
|
||||
className="flex space-x-1 sm:mt-0 sm:flex-row sm:items-center rounded-lg border border-green-400 dark:border-green-200 bg-green-200 px-2 py-2 text-gray-500 hover:bg-transparent hover:text-green-600 focus:outline-none focus:ring active:text-indigo-500"
|
||||
onClick={() => addToLibrary("comicvine", result)}
|
||||
>
|
||||
<i className="icon-[solar--add-square-bold-duotone] w-6 h-6 mr-2"></i>{" "}
|
||||
Mark as Wanted
|
||||
</button>
|
||||
<PopoverButton issuesCount={result.count_of_issues} />
|
||||
{/* onClick={() => addToLibrary("comicvine", result) */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import * as React from "react";
|
||||
import { IExtractedComicBookCoverFile } from "threetwo-ui-typings";
|
||||
import {
|
||||
removeLeadingPeriod,
|
||||
escapePoundSymbol,
|
||||
} from "../shared/utils/formatting.utils";
|
||||
import { isUndefined, isEmpty, isNil } from "lodash";
|
||||
import { Link } from "react-router-dom";
|
||||
import { LIBRARY_SERVICE_HOST } from "../constants/endpoints";
|
||||
import ellipsize from "ellipsize";
|
||||
|
||||
interface IProps {
|
||||
comicBookCoversMetadata?: IExtractedComicBookCoverFile;
|
||||
mongoObjId?: number;
|
||||
hasTitle: boolean;
|
||||
title?: string;
|
||||
isHorizontal: boolean;
|
||||
}
|
||||
interface IState {}
|
||||
|
||||
class Card extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
public drawCoverCard = (
|
||||
metadata: IExtractedComicBookCoverFile,
|
||||
): JSX.Element => {
|
||||
const encodedFilePath = encodeURI(
|
||||
`${LIBRARY_SERVICE_HOST}` + removeLeadingPeriod(metadata.path),
|
||||
);
|
||||
const filePath = escapePoundSymbol(encodedFilePath);
|
||||
return (
|
||||
<div>
|
||||
<div className="card generic-card">
|
||||
<div className={this.props.isHorizontal ? "is-horizontal" : ""}>
|
||||
<div className="card-image">
|
||||
<figure className="image">
|
||||
<img src={filePath} alt="Placeholder image" />
|
||||
</figure>
|
||||
</div>
|
||||
{this.props.hasTitle && (
|
||||
<div className="card-content">
|
||||
<ul>
|
||||
<Link to={"/comic/details/" + this.props.mongoObjId}>
|
||||
<li className="has-text-weight-semibold">
|
||||
{ellipsize(metadata.name, 18)}
|
||||
</li>
|
||||
</Link>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
{!isUndefined(this.props.comicBookCoversMetadata) &&
|
||||
!isEmpty(this.props.comicBookCoversMetadata) &&
|
||||
this.drawCoverCard(this.props.comicBookCoversMetadata)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Card;
|
||||
@@ -1,71 +1,42 @@
|
||||
import React, { ChangeEventHandler, useRef, useState } from "react";
|
||||
|
||||
import { format, isValid, parse, parseISO } from "date-fns";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { format } from "date-fns";
|
||||
import FocusTrap from "focus-trap-react";
|
||||
import { DayPicker, SelectSingleEventHandler } from "react-day-picker";
|
||||
import { usePopper } from "react-popper";
|
||||
import { ClassNames, DayPicker } from "react-day-picker";
|
||||
import { useFloating, offset, flip, autoUpdate } from "@floating-ui/react-dom";
|
||||
import styles from "react-day-picker/dist/style.module.css";
|
||||
|
||||
export const DatePickerDialog = (props) => {
|
||||
const { setter, apiAction } = props;
|
||||
const [selected, setSelected] = useState<Date>();
|
||||
const [isPopperOpen, setIsPopperOpen] = useState(false);
|
||||
|
||||
const popperRef = useRef<HTMLDivElement>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const customStyles = {
|
||||
container: {
|
||||
// Style for the entire container
|
||||
border: "1px solid #ccc",
|
||||
borderRadius: "4px",
|
||||
padding: "10px",
|
||||
width: "300px",
|
||||
},
|
||||
day: {
|
||||
// Style for individual days
|
||||
|
||||
padding: "5px",
|
||||
margin: "2px",
|
||||
},
|
||||
selected: {
|
||||
// Style for selected days
|
||||
backgroundColor: "#007bff",
|
||||
color: "#fff",
|
||||
},
|
||||
disabled: {
|
||||
// Style for disabled days
|
||||
color: "#ccc",
|
||||
},
|
||||
today: {
|
||||
// Style for today's date
|
||||
backgroundColor: "#f0f0f0",
|
||||
},
|
||||
dayWrapper: {
|
||||
// Style for the wrapper around each day
|
||||
display: "inline-block",
|
||||
},
|
||||
const classNames: ClassNames = {
|
||||
...styles,
|
||||
head: "custom-head",
|
||||
};
|
||||
|
||||
const popper = usePopper(popperRef.current, popperElement, {
|
||||
placement: "bottom-start",
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const { x, y, reference, floating, strategy, refs, update } = useFloating({
|
||||
placement: "bottom-end",
|
||||
middleware: [offset(10), flip()],
|
||||
strategy: "absolute",
|
||||
});
|
||||
|
||||
const closePopper = () => {
|
||||
setIsPopperOpen(false);
|
||||
buttonRef?.current?.focus();
|
||||
buttonRef.current?.focus();
|
||||
};
|
||||
|
||||
const handleButtonClick = () => {
|
||||
setIsPopperOpen(true);
|
||||
if (refs.reference.current && refs.floating.current) {
|
||||
autoUpdate(refs.reference.current, refs.floating.current, update);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDaySelect: SelectSingleEventHandler = (date) => {
|
||||
const handleDaySelect = (date) => {
|
||||
setSelected(date);
|
||||
if (date) {
|
||||
setter(format(date, "M-dd-yyyy"));
|
||||
setter(format(date, "yyyy-MM-dd"));
|
||||
apiAction();
|
||||
closePopper();
|
||||
} else {
|
||||
@@ -75,17 +46,14 @@ export const DatePickerDialog = (props) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div ref={popperRef}>
|
||||
<div ref={reference}>
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type="button"
|
||||
aria-label="Pick a date"
|
||||
onClick={handleButtonClick}
|
||||
className="flex space-x-1 sm:flex-row sm:items-center rounded-lg border border-green-400 dark:border-green-200 bg-green-200 px-2 py-1 text-gray-500 hover:bg-transparent hover:text-green-600 focus:outline-none focus:ring active:text-indigo-500"
|
||||
className="flex space-x-1 mb-2 sm:mt-0 sm:flex-row sm:items-center rounded-lg border border-green-400 dark:border-green-200 bg-green-200 px-2 py-1 text-gray-500 hover:bg-transparent hover:text-green-600 focus:outline-none focus:ring active:text-indigo-500"
|
||||
>
|
||||
<span className="pr-1 pt-0.5 h-8">
|
||||
<span className="icon-[solar--calendar-date-bold-duotone] w-6 h-6"></span>
|
||||
</span>
|
||||
Pick a date
|
||||
</button>
|
||||
</div>
|
||||
@@ -101,11 +69,14 @@ export const DatePickerDialog = (props) => {
|
||||
}}
|
||||
>
|
||||
<div
|
||||
tabIndex={-1}
|
||||
style={popper.styles.popper}
|
||||
className="bg-slate-200 mt-3 p-2 rounded-lg z-50"
|
||||
{...popper.attributes.popper}
|
||||
ref={setPopperElement}
|
||||
ref={floating}
|
||||
style={{
|
||||
position: strategy,
|
||||
zIndex: "999",
|
||||
borderRadius: "10px",
|
||||
boxShadow: "0 4px 6px rgba(0,0,0,0.1)", // Example of adding a shadow
|
||||
}}
|
||||
className="bg-slate-400 dark:bg-slate-500"
|
||||
role="dialog"
|
||||
aria-label="DayPicker calendar"
|
||||
>
|
||||
@@ -115,7 +86,7 @@ export const DatePickerDialog = (props) => {
|
||||
defaultMonth={selected}
|
||||
selected={selected}
|
||||
onSelect={handleDaySelect}
|
||||
styles={customStyles}
|
||||
classNames={classNames}
|
||||
/>
|
||||
</div>
|
||||
</FocusTrap>
|
||||
|
||||
@@ -1,312 +0,0 @@
|
||||
import React from "react";
|
||||
import { SearchBar } from "../GlobalSearchBar/SearchBar";
|
||||
import { DownloadProgressTick } from "../ComicDetail/DownloadProgressTick";
|
||||
import { Link } from "react-router-dom";
|
||||
import { isEmpty, isNil, isUndefined } from "lodash";
|
||||
import { format, fromUnixTime } from "date-fns";
|
||||
import { useStore } from "../../store/index";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
|
||||
const Navbar: React.FunctionComponent = (props) => {
|
||||
const {
|
||||
airDCPPSocketConnected,
|
||||
airDCPPDisconnectionInfo,
|
||||
airDCPPSessionInformation,
|
||||
airDCPPDownloadTick,
|
||||
importJobQueue,
|
||||
} = useStore(
|
||||
useShallow((state) => ({
|
||||
airDCPPSocketConnected: state.airDCPPSocketConnected,
|
||||
airDCPPDisconnectionInfo: state.airDCPPDisconnectionInfo,
|
||||
airDCPPSessionInformation: state.airDCPPSessionInformation,
|
||||
airDCPPDownloadTick: state.airDCPPDownloadTick,
|
||||
importJobQueue: state.importJobQueue,
|
||||
})),
|
||||
);
|
||||
// const downloadProgressTick = useSelector(
|
||||
// (state: RootState) => state.airdcpp.downloadProgressData,
|
||||
// );
|
||||
//
|
||||
// const airDCPPSocketConnectionStatus = useSelector(
|
||||
// (state: RootState) => state.airdcpp.isAirDCPPSocketConnected,
|
||||
// );
|
||||
// const airDCPPSessionInfo = useSelector(
|
||||
// (state: RootState) => state.airdcpp.airDCPPSessionInfo,
|
||||
// );
|
||||
// const socketDisconnectionReason = useSelector(
|
||||
// (state: RootState) => state.airdcpp.socketDisconnectionReason,
|
||||
// );
|
||||
|
||||
return (
|
||||
<nav className="navbar is-fixed-top">
|
||||
<div className="navbar-brand">
|
||||
<Link to="/dashboard" className="navbar-item">
|
||||
<img
|
||||
src="/src/client/assets/img/threetwo.svg"
|
||||
alt="ThreeTwo! A comic book curator"
|
||||
width="112"
|
||||
height="28"
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<a className="navbar-item is-hidden-desktop">
|
||||
<span className="icon">
|
||||
<i className="fas fa-github"></i>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a className="navbar-item is-hidden-desktop">
|
||||
<span className="icon">
|
||||
<i className="fas fa-twitter"></i>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<div className="navbar-burger burger" data-target="navMenubd-example">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="navMenubd-example" className="navbar-menu">
|
||||
<div className="navbar-start">
|
||||
<Link to="/" className="navbar-item">
|
||||
Dashboard
|
||||
</Link>
|
||||
|
||||
<Link to="/import" className="navbar-item">
|
||||
Import
|
||||
</Link>
|
||||
|
||||
<Link to="/library" className="navbar-item">
|
||||
Library
|
||||
</Link>
|
||||
|
||||
<Link to="/downloads" className="navbar-item">
|
||||
Downloads
|
||||
</Link>
|
||||
|
||||
{/* <SearchBar /> */}
|
||||
|
||||
<Link to="/search" className="navbar-item">
|
||||
Search ComicVine
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="navbar-end">
|
||||
<a className="navbar-item is-hidden-desktop-only"></a>
|
||||
|
||||
<div className="navbar-item has-dropdown is-hoverable">
|
||||
<a className="navbar-link is-arrowless">
|
||||
<i className="fa-solid fa-download"></i>
|
||||
{!isEmpty(airDCPPDownloadTick) && (
|
||||
<div className="pulsating-circle"></div>
|
||||
)}
|
||||
</a>
|
||||
{!isEmpty(airDCPPDownloadTick) ? (
|
||||
<div className="navbar-dropdown is-right is-boxed">
|
||||
<a className="navbar-item">
|
||||
<DownloadProgressTick data={airDCPPDownloadTick} />
|
||||
</a>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{!isUndefined(importJobQueue.status) &&
|
||||
location.hash !== "#/import" ? (
|
||||
<div className="navbar-item has-dropdown is-hoverable">
|
||||
<a className="navbar-link is-arrowless">
|
||||
<i className="fa-solid fa-file-import has-text-warning-dark"></i>
|
||||
</a>
|
||||
|
||||
<div className="navbar-dropdown is-right is-boxed">
|
||||
<a className="navbar-item">
|
||||
<ul>
|
||||
{importJobQueue.successfulJobCount > 0 ? (
|
||||
<li className="mb-2">
|
||||
<span className="tag is-success mr-2">
|
||||
{importJobQueue.successfulJobCount}
|
||||
</span>
|
||||
imported.
|
||||
</li>
|
||||
) : null}
|
||||
{importJobQueue.failedJobCount > 0 ? (
|
||||
<li>
|
||||
<span className="tag is-danger mr-2">
|
||||
{importJobQueue.failedJobCount}
|
||||
</span>
|
||||
failed to import.
|
||||
</li>
|
||||
) : null}
|
||||
</ul>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* AirDC++ socket connection status */}
|
||||
<div className="navbar-item has-dropdown is-hoverable">
|
||||
{!isUndefined(airDCPPSessionInformation.user) ? (
|
||||
<>
|
||||
<a className="navbar-link is-arrowless has-text-success">
|
||||
<i className="fa-solid fa-bolt"></i>
|
||||
</a>
|
||||
<div className="navbar-dropdown pr-2 pl-2 is-right airdcpp-status is-boxed">
|
||||
{/* AirDC++ Session Information */}
|
||||
|
||||
<p>
|
||||
Last login was{" "}
|
||||
<span className="tag">
|
||||
{format(
|
||||
fromUnixTime(
|
||||
airDCPPSessionInformation?.user.last_login,
|
||||
),
|
||||
"dd MMMM, yyyy",
|
||||
)}
|
||||
</span>
|
||||
</p>
|
||||
<hr className="navbar-divider" />
|
||||
<p>
|
||||
<span className="tag has-text-success">
|
||||
{airDCPPSessionInformation.user.username}
|
||||
</span>
|
||||
connected to{" "}
|
||||
<span className="tag has-text-success">
|
||||
{airDCPPSessionInformation.system_info.client_version}
|
||||
</span>{" "}
|
||||
with session ID{" "}
|
||||
<span className="tag has-text-success">
|
||||
{airDCPPSessionInformation.session_id}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<a className="navbar-link is-arrowless has-text-danger">
|
||||
<i className="fa-solid fa-bolt"></i>
|
||||
</a>
|
||||
<div className="navbar-dropdown pr-2 pl-2 is-right is-boxed">
|
||||
<pre>{JSON.stringify(airDCPPDisconnectionInfo, null, 2)}</pre>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="navbar-item has-dropdown is-hoverable is-mega">
|
||||
<div className="navbar-link flex">Blog</div>
|
||||
<div id="blogDropdown" className="navbar-dropdown">
|
||||
<div className="container is-fluid">
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
<h1 className="title is-6 is-mega-menu-title">
|
||||
Sub Menu Title
|
||||
</h1>
|
||||
<a className="navbar-item" href="/2017/08/03/list-of-tags/">
|
||||
<div className="navbar-content">
|
||||
<p>
|
||||
<small className="has-text-info">03 Aug 2017</small>
|
||||
</p>
|
||||
<p>New feature: list of tags</p>
|
||||
</div>
|
||||
</a>
|
||||
<a className="navbar-item" href="/2017/08/03/list-of-tags/">
|
||||
<div className="navbar-content">
|
||||
<p>
|
||||
<small className="has-text-info">03 Aug 2017</small>
|
||||
</p>
|
||||
<p>New feature: list of tags</p>
|
||||
</div>
|
||||
</a>
|
||||
<a className="navbar-item" href="/2017/08/03/list-of-tags/">
|
||||
<div className="navbar-content">
|
||||
<p>
|
||||
<small className="has-text-info">03 Aug 2017</small>
|
||||
</p>
|
||||
<p>New feature: list of tags</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div className="column">
|
||||
<h1 className="title is-6 is-mega-menu-title">
|
||||
Sub Menu Title
|
||||
</h1>
|
||||
|
||||
<a
|
||||
className="navbar-item "
|
||||
href="http://bulma.io/documentation/columns/basics/"
|
||||
>
|
||||
Columns
|
||||
</a>
|
||||
</div>
|
||||
<div className="column">
|
||||
<h1 className="title is-6 is-mega-menu-title">
|
||||
Sub Menu Title
|
||||
</h1>
|
||||
|
||||
<a className="navbar-item" href="/2017/08/03/list-of-tags/">
|
||||
<div className="navbar-content">
|
||||
<p>
|
||||
<small className="has-text-info">03 Aug 2017</small>
|
||||
</p>
|
||||
<p>New feature: list of tags</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div className="column">
|
||||
<h1 className="title is-6 is-mega-menu-title">
|
||||
Sub Menu Title
|
||||
</h1>
|
||||
<a
|
||||
className="navbar-item "
|
||||
href="/documentation/overview/start/"
|
||||
>
|
||||
Overview
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className="navbar-divider" />
|
||||
<div className="navbar-item">
|
||||
<div className="navbar-content">
|
||||
<div className="level is-mobile">
|
||||
<div className="level-left">
|
||||
<div className="level-item">
|
||||
<strong>Stay up to date!</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div className="level-right">
|
||||
<div className="level-item">
|
||||
<a
|
||||
className="button bd-is-rss is-small"
|
||||
href="http://bulma.io/atom.xml"
|
||||
>
|
||||
<span className="icon is-small">
|
||||
<i className="fa fa-rss"></i>
|
||||
</span>
|
||||
<span>Subscribe</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="navbar-item">
|
||||
<div className="field is-grouped">
|
||||
<p className="control">
|
||||
<Link to="/settings" className="navbar-item">
|
||||
Settings
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
||||
42
src/client/components/shared/PopoverButton.tsx
Normal file
42
src/client/components/shared/PopoverButton.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React, { useState } from "react";
|
||||
import { useFloating, offset, flip } from "@floating-ui/react-dom";
|
||||
|
||||
const PopoverButton = ({ issuesCount }) => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
// Use destructuring to obtain the reference and floating setters, among other values.
|
||||
const { x, y, refs, strategy, floatingStyles } = useFloating({
|
||||
placement: "right",
|
||||
middleware: [offset(8), flip()],
|
||||
strategy: "absolute",
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
{/* Apply the reference setter directly to the ref prop */}
|
||||
<button
|
||||
ref={refs.setReference}
|
||||
onMouseEnter={() => setIsVisible(true)}
|
||||
onMouseLeave={() => setIsVisible(false)}
|
||||
onFocus={() => setIsVisible(true)}
|
||||
onBlur={() => setIsVisible(false)}
|
||||
aria-describedby="popover"
|
||||
className="flex space-x-1 sm:mt-0 sm:flex-row sm:items-center rounded-lg border border-green-400 dark:border-green-200 bg-green-200 px-2 py-2 text-gray-500 hover:bg-transparent hover:text-green-600 focus:outline-none focus:ring active:text-indigo-500"
|
||||
>
|
||||
<i className="icon-[solar--add-square-bold-duotone] w-6 h-6 mr-2"></i>{" "}
|
||||
Mark as Wanted
|
||||
</button>
|
||||
{isVisible && (
|
||||
<div
|
||||
ref={refs.setFloating} // Apply the floating setter directly to the ref prop
|
||||
style={floatingStyles}
|
||||
className="text-sm bg-slate-400 p-2 rounded-md"
|
||||
role="tooltip"
|
||||
>
|
||||
Adding this volume will add all {issuesCount} issues to your wanted
|
||||
list.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PopoverButton;
|
||||
@@ -1,11 +0,0 @@
|
||||
import React, { createContext } from "react";
|
||||
|
||||
export const SocketIOContext = createContext({});
|
||||
|
||||
export const SocketIOProvider = ({ children, socket }) => {
|
||||
return (
|
||||
<SocketIOContext.Provider value={socket}>
|
||||
{children}
|
||||
</SocketIOContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
import React, { useEffect } from "react";
|
||||
import BlazeSlider from "blaze-slider";
|
||||
|
||||
export const useBlazeSlider = (config) => {
|
||||
const sliderRef = React.useRef();
|
||||
const elRef = React.useRef();
|
||||
|
||||
useEffect(() => {
|
||||
// if not already initialized
|
||||
if (!sliderRef.current) {
|
||||
sliderRef.current = new BlazeSlider(elRef.current, config);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return elRef;
|
||||
};
|
||||
@@ -1,133 +0,0 @@
|
||||
import {
|
||||
AIRDCPP_SEARCH_IN_PROGRESS,
|
||||
AIRDCPP_SEARCH_RESULTS_ADDED,
|
||||
AIRDCPP_SEARCH_RESULTS_UPDATED,
|
||||
AIRDCPP_HUB_SEARCHES_SENT,
|
||||
AIRDCPP_RESULT_DOWNLOAD_INITIATED,
|
||||
AIRDCPP_DOWNLOAD_PROGRESS_TICK,
|
||||
AIRDCPP_FILE_DOWNLOAD_COMPLETED,
|
||||
AIRDCPP_BUNDLES_FETCHED,
|
||||
AIRDCPP_TRANSFERS_FETCHED,
|
||||
LIBRARY_ISSUE_BUNDLES,
|
||||
AIRDCPP_SOCKET_CONNECTED,
|
||||
AIRDCPP_SOCKET_DISCONNECTED,
|
||||
} from "../constants/action-types";
|
||||
import { LOCATION_CHANGE } from "redux-first-history";
|
||||
import { isNil, isUndefined } from "lodash";
|
||||
import { difference } from "../shared/utils/object.utils";
|
||||
|
||||
const initialState = {
|
||||
searchResults: [],
|
||||
isAirDCPPSearchInProgress: false,
|
||||
searchInfo: null,
|
||||
searchInstance: null,
|
||||
downloadResult: null,
|
||||
bundleDBImportResult: null,
|
||||
downloadFileStatus: {},
|
||||
bundles: [],
|
||||
transfers: [],
|
||||
isAirDCPPSocketConnected: false,
|
||||
airDCPPSessionInfo: {},
|
||||
socketDisconnectionReason: {},
|
||||
};
|
||||
|
||||
function airdcppReducer(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case AIRDCPP_SEARCH_RESULTS_ADDED:
|
||||
return {
|
||||
...state,
|
||||
searchResults: [...state.searchResults, action.groupedResult],
|
||||
isAirDCPPSearchInProgress: true,
|
||||
};
|
||||
case AIRDCPP_SEARCH_RESULTS_UPDATED:
|
||||
const bundleToUpdateIndex = state.searchResults.findIndex(
|
||||
(bundle) => bundle.result.id === action.groupedResult.result.id,
|
||||
);
|
||||
const updatedState = [...state.searchResults];
|
||||
|
||||
if (
|
||||
!isNil(
|
||||
difference(updatedState[bundleToUpdateIndex], action.groupedResult),
|
||||
)
|
||||
) {
|
||||
updatedState[bundleToUpdateIndex] = action.groupedResult;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
searchResults: updatedState,
|
||||
};
|
||||
case AIRDCPP_SEARCH_IN_PROGRESS:
|
||||
return {
|
||||
...state,
|
||||
isAirDCPPSearchInProgress: true,
|
||||
};
|
||||
case AIRDCPP_HUB_SEARCHES_SENT:
|
||||
return {
|
||||
...state,
|
||||
isAirDCPPSearchInProgress: false,
|
||||
searchInfo: action.searchInfo,
|
||||
searchInstance: action.instance,
|
||||
};
|
||||
case AIRDCPP_RESULT_DOWNLOAD_INITIATED:
|
||||
return {
|
||||
...state,
|
||||
downloadResult: action.downloadResult,
|
||||
bundleDBImportResult: action.bundleDBImportResult,
|
||||
};
|
||||
case AIRDCPP_DOWNLOAD_PROGRESS_TICK:
|
||||
return {
|
||||
...state,
|
||||
downloadProgressData: action.downloadProgressData,
|
||||
};
|
||||
case AIRDCPP_BUNDLES_FETCHED:
|
||||
return {
|
||||
...state,
|
||||
bundles: action.bundles,
|
||||
};
|
||||
case LIBRARY_ISSUE_BUNDLES:
|
||||
return {
|
||||
...state,
|
||||
issue_bundles: action.issue_bundles,
|
||||
};
|
||||
case AIRDCPP_FILE_DOWNLOAD_COMPLETED:
|
||||
console.log("COMPLETED", action);
|
||||
return {
|
||||
...state,
|
||||
};
|
||||
case AIRDCPP_TRANSFERS_FETCHED:
|
||||
return {
|
||||
...state,
|
||||
transfers: action.bundles,
|
||||
};
|
||||
|
||||
case AIRDCPP_SOCKET_CONNECTED:
|
||||
return {
|
||||
...state,
|
||||
isAirDCPPSocketConnected: true,
|
||||
airDCPPSessionInfo: action.data,
|
||||
};
|
||||
|
||||
case AIRDCPP_SOCKET_DISCONNECTED:
|
||||
return {
|
||||
...state,
|
||||
isAirDCPPSocketConnected: false,
|
||||
socketDisconnectionReason: action.data,
|
||||
};
|
||||
case LOCATION_CHANGE:
|
||||
return {
|
||||
...state,
|
||||
searchResults: [],
|
||||
isAirDCPPSearchInProgress: false,
|
||||
searchInfo: null,
|
||||
searchInstance: null,
|
||||
downloadResult: null,
|
||||
bundleDBImportResult: null,
|
||||
// bundles: [],
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default airdcppReducer;
|
||||
@@ -1,138 +0,0 @@
|
||||
import { isEmpty } from "lodash";
|
||||
import {
|
||||
CV_API_CALL_IN_PROGRESS,
|
||||
CV_SEARCH_SUCCESS,
|
||||
CV_CLEANUP,
|
||||
IMS_COMIC_BOOK_DB_OBJECT_FETCHED,
|
||||
IMS_COMIC_BOOKS_DB_OBJECTS_FETCHED,
|
||||
IMS_COMIC_BOOK_DB_OBJECT_CALL_IN_PROGRESS,
|
||||
CV_ISSUES_METADATA_CALL_IN_PROGRESS,
|
||||
CV_ISSUES_MATCHES_IN_LIBRARY_FETCHED,
|
||||
CV_ISSUES_FOR_VOLUME_IN_LIBRARY_SUCCESS,
|
||||
CV_WEEKLY_PULLLIST_FETCHED,
|
||||
CV_WEEKLY_PULLLIST_CALL_IN_PROGRESS,
|
||||
LIBRARY_STATISTICS_CALL_IN_PROGRESS,
|
||||
LIBRARY_STATISTICS_FETCHED,
|
||||
} from "../constants/action-types";
|
||||
|
||||
const initialState = {
|
||||
pullList: [],
|
||||
libraryStatistics: [],
|
||||
searchResults: [],
|
||||
searchQuery: {},
|
||||
inProgress: false,
|
||||
comicBookDetail: {},
|
||||
comicBooksDetails: [],
|
||||
issuesForVolume: [],
|
||||
IMS_inProgress: false,
|
||||
};
|
||||
|
||||
function comicinfoReducer(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case CV_API_CALL_IN_PROGRESS:
|
||||
return {
|
||||
...state,
|
||||
inProgress: true,
|
||||
};
|
||||
case CV_SEARCH_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
searchResults: action.searchResults,
|
||||
searchQuery: action.searchQueryObject,
|
||||
inProgress: false,
|
||||
};
|
||||
case IMS_COMIC_BOOK_DB_OBJECT_CALL_IN_PROGRESS:
|
||||
return {
|
||||
...state,
|
||||
IMS_inProgress: true,
|
||||
};
|
||||
|
||||
case IMS_COMIC_BOOK_DB_OBJECT_FETCHED:
|
||||
return {
|
||||
...state,
|
||||
comicBookDetail: action.comicBookDetail,
|
||||
IMS_inProgress: false,
|
||||
};
|
||||
case IMS_COMIC_BOOKS_DB_OBJECTS_FETCHED:
|
||||
return {
|
||||
...state,
|
||||
comicBooksDetails: action.comicBooks,
|
||||
IMS_inProgress: false,
|
||||
};
|
||||
case CV_CLEANUP:
|
||||
return {
|
||||
...state,
|
||||
searchResults: [],
|
||||
searchQuery: {},
|
||||
issuesForVolume: [],
|
||||
};
|
||||
case CV_ISSUES_METADATA_CALL_IN_PROGRESS:
|
||||
return {
|
||||
inProgress: true,
|
||||
...state,
|
||||
};
|
||||
|
||||
case CV_ISSUES_FOR_VOLUME_IN_LIBRARY_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
issuesForVolume: action.issues,
|
||||
inProgress: false,
|
||||
};
|
||||
|
||||
case CV_ISSUES_MATCHES_IN_LIBRARY_FETCHED:
|
||||
const updatedState = [...state.issuesForVolume];
|
||||
action.matches.map((match) => {
|
||||
updatedState.map((issue, idx) => {
|
||||
const matches = [];
|
||||
if (!isEmpty(match.hits.hits)) {
|
||||
return match.hits.hits.map((hit) => {
|
||||
if (
|
||||
parseInt(issue.issue_number, 10) ===
|
||||
hit._source.inferredMetadata.issue.number
|
||||
) {
|
||||
matches.push(hit);
|
||||
const updatedIssueResult = { ...issue, matches };
|
||||
updatedState[idx] = updatedIssueResult;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
return {
|
||||
...state,
|
||||
issuesForVolume: updatedState,
|
||||
};
|
||||
|
||||
case CV_WEEKLY_PULLLIST_CALL_IN_PROGRESS:
|
||||
return {
|
||||
inProgress: true,
|
||||
...state,
|
||||
};
|
||||
case CV_WEEKLY_PULLLIST_FETCHED: {
|
||||
const foo = [];
|
||||
action.data.map((item) => {
|
||||
foo.push({issue: item})
|
||||
});
|
||||
return {
|
||||
...state,
|
||||
inProgress: false,
|
||||
pullList: foo,
|
||||
};
|
||||
}
|
||||
case LIBRARY_STATISTICS_CALL_IN_PROGRESS:
|
||||
return {
|
||||
inProgress: true,
|
||||
...state,
|
||||
};
|
||||
case LIBRARY_STATISTICS_FETCHED:
|
||||
return {
|
||||
...state,
|
||||
inProgress: false,
|
||||
libraryStatistics: action.data,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default comicinfoReducer;
|
||||
@@ -1,334 +0,0 @@
|
||||
import { isUndefined, map } from "lodash";
|
||||
import { LOCATION_CHANGE } from "redux-first-history";
|
||||
import { determineCoverFile } from "../shared/utils/metadata.utils";
|
||||
import {
|
||||
IMS_COMICBOOK_METADATA_FETCHED,
|
||||
IMS_RAW_IMPORT_SUCCESSFUL,
|
||||
IMS_RAW_IMPORT_FAILED,
|
||||
IMS_RECENT_COMICS_FETCHED,
|
||||
IMS_WANTED_COMICS_FETCHED,
|
||||
WANTED_COMICS_FETCHED,
|
||||
IMS_CV_METADATA_IMPORT_SUCCESSFUL,
|
||||
IMS_CV_METADATA_IMPORT_FAILED,
|
||||
IMS_CV_METADATA_IMPORT_CALL_IN_PROGRESS,
|
||||
IMS_COMIC_BOOK_GROUPS_CALL_IN_PROGRESS,
|
||||
IMS_COMIC_BOOK_GROUPS_FETCHED,
|
||||
IMS_COMIC_BOOK_GROUPS_CALL_FAILED,
|
||||
IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_IN_PROGRESS,
|
||||
LS_IMPORT,
|
||||
LS_COVER_EXTRACTED,
|
||||
LS_COVER_EXTRACTION_FAILED,
|
||||
LS_COMIC_ADDED,
|
||||
IMG_ANALYSIS_CALL_IN_PROGRESS,
|
||||
IMG_ANALYSIS_DATA_FETCH_SUCCESS,
|
||||
SS_SEARCH_RESULTS_FETCHED,
|
||||
SS_SEARCH_IN_PROGRESS,
|
||||
FILEOPS_STATE_RESET,
|
||||
LS_IMPORT_CALL_IN_PROGRESS,
|
||||
SS_SEARCH_FAILED,
|
||||
SS_SEARCH_RESULTS_FETCHED_SPECIAL,
|
||||
VOLUMES_FETCHED,
|
||||
COMICBOOK_EXTRACTION_SUCCESS,
|
||||
LIBRARY_SERVICE_HEALTH,
|
||||
LS_IMPORT_QUEUE_DRAINED,
|
||||
LS_SET_QUEUE_STATUS,
|
||||
RESTORE_JOB_COUNTS_AFTER_SESSION_RESTORATION,
|
||||
LS_IMPORT_JOB_STATISTICS_FETCHED,
|
||||
} from "../constants/action-types";
|
||||
import { removeLeadingPeriod } from "../shared/utils/formatting.utils";
|
||||
import { LIBRARY_SERVICE_HOST } from "../constants/endpoints";
|
||||
|
||||
const initialState = {
|
||||
IMSCallInProgress: false,
|
||||
IMGCallInProgress: false,
|
||||
SSCallInProgress: false,
|
||||
imageAnalysisResults: {},
|
||||
comicBookExtractionInProgress: false,
|
||||
LSQueueImportStatus: undefined,
|
||||
comicBookMetadata: [],
|
||||
comicVolumeGroups: [],
|
||||
isSocketConnected: false,
|
||||
isComicVineMetadataImportInProgress: false,
|
||||
comicVineMetadataImportError: {},
|
||||
rawImportError: {},
|
||||
extractedComicBookArchive: {
|
||||
reading: [],
|
||||
analysis: [],
|
||||
},
|
||||
recentComics: [],
|
||||
wantedComics: [],
|
||||
libraryComics: [],
|
||||
volumes: [],
|
||||
librarySearchResultsFormatted: [],
|
||||
lastQueueJob: "",
|
||||
successfulJobCount: 0,
|
||||
failedJobCount: 0,
|
||||
importJobStatistics: [],
|
||||
libraryQueueResults: [],
|
||||
librarySearchError: {},
|
||||
libraryServiceStatus: {},
|
||||
};
|
||||
function fileOpsReducer(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case IMS_COMICBOOK_METADATA_FETCHED:
|
||||
return {
|
||||
...state,
|
||||
comicBookMetadata: [...state.comicBookMetadata, action.data],
|
||||
IMSCallInProgress: false,
|
||||
};
|
||||
|
||||
case LS_IMPORT_CALL_IN_PROGRESS: {
|
||||
return {
|
||||
...state,
|
||||
IMSCallInProgress: true,
|
||||
};
|
||||
}
|
||||
case IMS_RAW_IMPORT_SUCCESSFUL:
|
||||
return {
|
||||
...state,
|
||||
rawImportDetails: action.rawImportDetails,
|
||||
};
|
||||
case IMS_RAW_IMPORT_FAILED:
|
||||
return {
|
||||
...state,
|
||||
rawImportErorr: action.rawImportError,
|
||||
};
|
||||
case IMS_RECENT_COMICS_FETCHED:
|
||||
return {
|
||||
...state,
|
||||
recentComics: action.data.docs,
|
||||
};
|
||||
case IMS_WANTED_COMICS_FETCHED:
|
||||
return {
|
||||
...state,
|
||||
wantedComics: action.data,
|
||||
};
|
||||
case IMS_CV_METADATA_IMPORT_SUCCESSFUL:
|
||||
return {
|
||||
...state,
|
||||
isComicVineMetadataImportInProgress: false,
|
||||
comicVineMetadataImportDetails: action.importResult,
|
||||
};
|
||||
case IMS_CV_METADATA_IMPORT_CALL_IN_PROGRESS:
|
||||
return {
|
||||
...state,
|
||||
isComicVineMetadataImportInProgress: true,
|
||||
};
|
||||
case IMS_CV_METADATA_IMPORT_FAILED:
|
||||
return {
|
||||
...state,
|
||||
isComicVineMetadataImportInProgress: false,
|
||||
comicVineMetadataImportError: action.importError,
|
||||
};
|
||||
case IMS_COMIC_BOOK_GROUPS_CALL_IN_PROGRESS: {
|
||||
return {
|
||||
...state,
|
||||
IMSCallInProgress: true,
|
||||
};
|
||||
}
|
||||
case IMS_COMIC_BOOK_GROUPS_FETCHED: {
|
||||
return {
|
||||
...state,
|
||||
comicVolumeGroups: action.data,
|
||||
IMSCallInProgress: false,
|
||||
};
|
||||
}
|
||||
case IMS_COMIC_BOOK_GROUPS_CALL_FAILED: {
|
||||
return {
|
||||
...state,
|
||||
IMSCallInProgress: false,
|
||||
error: action.error,
|
||||
};
|
||||
}
|
||||
case IMS_COMIC_BOOK_ARCHIVE_EXTRACTION_CALL_IN_PROGRESS: {
|
||||
return {
|
||||
...state,
|
||||
comicBookExtractionInProgress: true,
|
||||
};
|
||||
}
|
||||
|
||||
case LOCATION_CHANGE: {
|
||||
return {
|
||||
...state,
|
||||
extractedComicBookArchive: [],
|
||||
};
|
||||
}
|
||||
|
||||
case LS_IMPORT: {
|
||||
return {
|
||||
...state,
|
||||
LSQueueImportStatus: "running",
|
||||
};
|
||||
}
|
||||
|
||||
case LS_COVER_EXTRACTED: {
|
||||
if (state.recentComics.length === 5) {
|
||||
state.recentComics.pop();
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
successfulJobCount: action.completedJobCount,
|
||||
lastQueueJob: action.importResult.rawFileDetails.name,
|
||||
recentComics: [...state.recentComics, action.importResult],
|
||||
};
|
||||
}
|
||||
|
||||
case LS_COVER_EXTRACTION_FAILED: {
|
||||
return {
|
||||
...state,
|
||||
failedJobCount: action.failedJobCount,
|
||||
};
|
||||
}
|
||||
|
||||
case LS_IMPORT_QUEUE_DRAINED: {
|
||||
localStorage.removeItem("sessionId");
|
||||
return {
|
||||
...state,
|
||||
LSQueueImportStatus: "drained",
|
||||
};
|
||||
}
|
||||
|
||||
case RESTORE_JOB_COUNTS_AFTER_SESSION_RESTORATION: {
|
||||
console.log("Restoring state for an active import in progress...");
|
||||
return {
|
||||
...state,
|
||||
successfulJobCount: action.completedJobCount,
|
||||
failedJobCount: action.failedJobCount,
|
||||
LSQueueImportStatus: action.queueStatus,
|
||||
};
|
||||
}
|
||||
|
||||
case LS_SET_QUEUE_STATUS: {
|
||||
return {
|
||||
...state,
|
||||
LSQueueImportStatus: action.data.queueStatus,
|
||||
};
|
||||
}
|
||||
|
||||
case LS_IMPORT_JOB_STATISTICS_FETCHED: {
|
||||
return {
|
||||
...state,
|
||||
importJobStatistics: action.data,
|
||||
};
|
||||
}
|
||||
|
||||
case COMICBOOK_EXTRACTION_SUCCESS: {
|
||||
const comicBookPages: string[] = [];
|
||||
map(action.result.files, (page) => {
|
||||
const pageFilePath = removeLeadingPeriod(page);
|
||||
const imagePath = encodeURI(`${LIBRARY_SERVICE_HOST}${pageFilePath}`);
|
||||
comicBookPages.push(imagePath);
|
||||
});
|
||||
|
||||
switch (action.result.purpose) {
|
||||
case "reading":
|
||||
return {
|
||||
...state,
|
||||
extractedComicBookArchive: {
|
||||
reading: comicBookPages,
|
||||
},
|
||||
comicBookExtractionInProgress: false,
|
||||
};
|
||||
|
||||
case "analysis":
|
||||
return {
|
||||
...state,
|
||||
extractedComicBookArchive: {
|
||||
analysis: comicBookPages,
|
||||
},
|
||||
comicBookExtractionInProgress: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
case LS_COMIC_ADDED: {
|
||||
return {
|
||||
...state,
|
||||
};
|
||||
}
|
||||
case IMG_ANALYSIS_CALL_IN_PROGRESS: {
|
||||
return {
|
||||
...state,
|
||||
IMGCallInProgress: true,
|
||||
};
|
||||
}
|
||||
case IMG_ANALYSIS_DATA_FETCH_SUCCESS: {
|
||||
return {
|
||||
...state,
|
||||
imageAnalysisResults: action.result,
|
||||
};
|
||||
}
|
||||
|
||||
case SS_SEARCH_IN_PROGRESS: {
|
||||
return {
|
||||
...state,
|
||||
SSCallInProgress: true,
|
||||
};
|
||||
}
|
||||
|
||||
case SS_SEARCH_RESULTS_FETCHED: {
|
||||
return {
|
||||
...state,
|
||||
libraryComics: action.data,
|
||||
SSCallInProgress: false,
|
||||
};
|
||||
}
|
||||
case SS_SEARCH_RESULTS_FETCHED_SPECIAL: {
|
||||
const foo = [];
|
||||
if (!isUndefined(action.data.hits)) {
|
||||
map(action.data.hits, ({ _source }) => {
|
||||
foo.push(_source);
|
||||
});
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
librarySearchResultsFormatted: foo,
|
||||
SSCallInProgress: false,
|
||||
};
|
||||
}
|
||||
case WANTED_COMICS_FETCHED: {
|
||||
const foo = [];
|
||||
if (!isUndefined(action.data.hits)) {
|
||||
map(action.data.hits, ({ _source }) => {
|
||||
foo.push(_source);
|
||||
});
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
wantedComics: foo,
|
||||
SSCallInProgress: false,
|
||||
};
|
||||
}
|
||||
|
||||
case VOLUMES_FETCHED:
|
||||
return {
|
||||
...state,
|
||||
volumes: action.data,
|
||||
SSCallInProgress: false,
|
||||
};
|
||||
|
||||
case SS_SEARCH_FAILED: {
|
||||
return {
|
||||
...state,
|
||||
librarySearchError: action.data,
|
||||
SSCallInProgress: false,
|
||||
};
|
||||
}
|
||||
case LIBRARY_SERVICE_HEALTH: {
|
||||
return {
|
||||
...state,
|
||||
libraryServiceStatus: action.status,
|
||||
};
|
||||
}
|
||||
case FILEOPS_STATE_RESET: {
|
||||
return {
|
||||
...state,
|
||||
imageAnalysisResults: {},
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default fileOpsReducer;
|
||||
@@ -1,11 +0,0 @@
|
||||
import comicinfoReducer from "../reducers/comicinfo.reducer";
|
||||
import fileOpsReducer from "../reducers/fileops.reducer";
|
||||
import airdcppReducer from "../reducers/airdcpp.reducer";
|
||||
// import settingsReducer from "../reducers/settings.reducer";
|
||||
|
||||
export const reducers = {
|
||||
comicInfo: comicinfoReducer,
|
||||
fileOps: fileOpsReducer,
|
||||
airdcpp: airdcppReducer,
|
||||
// settings: settingsReducer,
|
||||
};
|
||||
@@ -1,67 +0,0 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
|
||||
import { RootState } from "../store";
|
||||
import { isUndefined } from "lodash";
|
||||
import { SETTINGS_SERVICE_BASE_URI } from "../constants/endpoints";
|
||||
|
||||
export interface InitialState {
|
||||
data: object;
|
||||
inProgress: boolean;
|
||||
dbFlushed: boolean;
|
||||
torrentsList: Array<any>;
|
||||
}
|
||||
const initialState: InitialState = {
|
||||
data: {},
|
||||
inProgress: false,
|
||||
dbFlushed: false,
|
||||
torrentsList: [],
|
||||
};
|
||||
|
||||
export const settingsSlice = createSlice({
|
||||
name: "settings",
|
||||
initialState,
|
||||
reducers: {
|
||||
SETTINGS_CALL_IN_PROGRESS: (state) => {
|
||||
state.inProgress = true;
|
||||
},
|
||||
|
||||
SETTINGS_OBJECT_FETCHED: (state, action) => {
|
||||
state.data = action.payload;
|
||||
state.inProgress = false;
|
||||
},
|
||||
|
||||
SETTINGS_OBJECT_DELETED: (state, action) => {
|
||||
state.data = action.payload;
|
||||
state.inProgress = false;
|
||||
},
|
||||
|
||||
SETTINGS_DB_FLUSH_SUCCESS: (state, action) => {
|
||||
state.dbFlushed = action.payload;
|
||||
state.inProgress = false;
|
||||
},
|
||||
|
||||
SETTINGS_QBITTORRENT_TORRENTS_LIST_FETCHED: (state, action) => {
|
||||
console.log(state);
|
||||
console.log(action);
|
||||
state.torrentsList = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
SETTINGS_CALL_IN_PROGRESS,
|
||||
SETTINGS_OBJECT_FETCHED,
|
||||
SETTINGS_OBJECT_DELETED,
|
||||
SETTINGS_DB_FLUSH_SUCCESS,
|
||||
SETTINGS_QBITTORRENT_TORRENTS_LIST_FETCHED,
|
||||
} = settingsSlice.actions;
|
||||
|
||||
// Other code such as selectors can use the imported `RootState` type
|
||||
export const torrentsList = (state: RootState) => state.settings.torrentsList;
|
||||
export const qBittorrentSettings = (state: RootState) => {
|
||||
console.log(state);
|
||||
if (!isUndefined(state.settings?.data?.bittorrent)) {
|
||||
return state.settings?.data?.bittorrent.client.host;
|
||||
}
|
||||
};
|
||||
export default settingsSlice.reducer;
|
||||
@@ -1,11 +0,0 @@
|
||||
const socketIOMiddleware = (socket) => {
|
||||
return (store) => (next) => (action) => {
|
||||
if (action.type === "EMIT_SOCKET_EVENT") {
|
||||
const { event, data } = action.payload;
|
||||
socket.emit(event, data);
|
||||
}
|
||||
return next(action);
|
||||
};
|
||||
};
|
||||
|
||||
export default socketIOMiddleware;
|
||||
@@ -1,10 +0,0 @@
|
||||
import io from "socket.io-client";
|
||||
import { SOCKET_BASE_URI } from "../../constants/endpoints";
|
||||
const sessionId = localStorage.getItem("sessionId");
|
||||
const socketIOConnectionInstance = io(SOCKET_BASE_URI, {
|
||||
transports: ["websocket"],
|
||||
withCredentials: true,
|
||||
query: { sessionId },
|
||||
});
|
||||
|
||||
export default socketIOConnectionInstance;
|
||||
@@ -16,6 +16,7 @@ export const detectIssueTypes = (deck: string): any => {
|
||||
const matches = map(issueTypeMatchers, (matcher) => {
|
||||
return getIssueTypeDisplayName(deck, matcher.regex, matcher.displayName);
|
||||
});
|
||||
|
||||
return compact(matches)[0];
|
||||
};
|
||||
|
||||
|
||||
42
yarn.lock
42
yarn.lock
@@ -1509,7 +1509,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@fal-works/esbuild-plugin-global-externals/-/esbuild-plugin-global-externals-2.1.2.tgz#c05ed35ad82df8e6ac616c68b92c2282bd083ba4"
|
||||
integrity sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ==
|
||||
|
||||
"@floating-ui/core@^1.6.0":
|
||||
"@floating-ui/core@^1.0.0", "@floating-ui/core@^1.6.0":
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.0.tgz#fa41b87812a16bf123122bf945946bae3fdf7fc1"
|
||||
integrity sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==
|
||||
@@ -1524,6 +1524,14 @@
|
||||
"@floating-ui/core" "^1.6.0"
|
||||
"@floating-ui/utils" "^0.2.1"
|
||||
|
||||
"@floating-ui/dom@^1.6.1":
|
||||
version "1.6.3"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.3.tgz#954e46c1dd3ad48e49db9ada7218b0985cee75ef"
|
||||
integrity sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==
|
||||
dependencies:
|
||||
"@floating-ui/core" "^1.0.0"
|
||||
"@floating-ui/utils" "^0.2.0"
|
||||
|
||||
"@floating-ui/react-dom@^2.0.0":
|
||||
version "2.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.7.tgz#873e0a55a25d8ddbbccd159d6ab4a4b98eb05494"
|
||||
@@ -1531,7 +1539,23 @@
|
||||
dependencies:
|
||||
"@floating-ui/dom" "^1.6.0"
|
||||
|
||||
"@floating-ui/utils@^0.2.1":
|
||||
"@floating-ui/react-dom@^2.0.8":
|
||||
version "2.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.8.tgz#afc24f9756d1b433e1fe0d047c24bd4d9cefaa5d"
|
||||
integrity sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==
|
||||
dependencies:
|
||||
"@floating-ui/dom" "^1.6.1"
|
||||
|
||||
"@floating-ui/react@^0.26.12":
|
||||
version "0.26.12"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.26.12.tgz#6908f774d8e3167d89b37fd83be975c7e5d8be99"
|
||||
integrity sha512-D09o62HrWdIkstF2kGekIKAC0/N/Dl6wo3CQsnLcOmO3LkW6Ik8uIb3kw8JYkwxNCcg+uJ2bpWUiIijTBep05w==
|
||||
dependencies:
|
||||
"@floating-ui/react-dom" "^2.0.0"
|
||||
"@floating-ui/utils" "^0.2.0"
|
||||
tabbable "^6.0.0"
|
||||
|
||||
"@floating-ui/utils@^0.2.0", "@floating-ui/utils@^0.2.1":
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2"
|
||||
integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==
|
||||
@@ -8997,7 +9021,7 @@ react-element-to-jsx-string@^15.0.0:
|
||||
is-plain-object "5.0.0"
|
||||
react-is "18.1.0"
|
||||
|
||||
react-fast-compare@^3.0.1, react-fast-compare@^3.2.0:
|
||||
react-fast-compare@^3.2.0:
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49"
|
||||
integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==
|
||||
@@ -9070,14 +9094,6 @@ react-modal@^3.14.3, react-modal@^3.15.1:
|
||||
react-lifecycles-compat "^3.0.0"
|
||||
warning "^4.0.3"
|
||||
|
||||
react-popper@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.3.0.tgz#17891c620e1320dce318bad9fede46a5f71c70ba"
|
||||
integrity sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==
|
||||
dependencies:
|
||||
react-fast-compare "^3.0.1"
|
||||
warning "^4.0.2"
|
||||
|
||||
react-refresh@^0.14.0:
|
||||
version "0.14.0"
|
||||
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e"
|
||||
@@ -10023,7 +10039,7 @@ synchronous-promise@^2.0.15:
|
||||
resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.17.tgz#38901319632f946c982152586f2caf8ddc25c032"
|
||||
integrity sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g==
|
||||
|
||||
tabbable@^6.2.0:
|
||||
tabbable@^6.0.0, tabbable@^6.2.0:
|
||||
version "6.2.0"
|
||||
resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.2.0.tgz#732fb62bc0175cfcec257330be187dcfba1f3b97"
|
||||
integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==
|
||||
@@ -10673,7 +10689,7 @@ walker@^1.0.8:
|
||||
dependencies:
|
||||
makeerror "1.0.12"
|
||||
|
||||
warning@^4.0.2, warning@^4.0.3:
|
||||
warning@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
|
||||
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
|
||||
|
||||
Reference in New Issue
Block a user