Compare commits
21 Commits
airdcpp-re
...
graphql-re
| Author | SHA1 | Date | |
|---|---|---|---|
| ea66419f33 | |||
| dc014a08ce | |||
| 90d6562f45 | |||
| 3563fef461 | |||
| 2968987c6b | |||
| 924ffae07e | |||
| 80f0ced0b0 | |||
|
|
0def39cd73 | ||
|
|
1c60837ec9 | ||
|
|
d42a700cec | ||
|
|
40fd8ede5d | ||
|
|
24b09c9c5e | ||
| c176dab78b | |||
| c0b189c9e6 | |||
| f3333b5c2c | |||
| 2ce90d94c0 | |||
| 0e445ba3d4 | |||
| 9c5fb93d5b | |||
| 338a46224d | |||
|
|
0615d08e7d | ||
| 21a127509b |
31
Dockerfile
31
Dockerfile
@@ -1,23 +1,36 @@
|
|||||||
FROM node:18.15.0-alpine
|
# Use Node.js 22 as the base image
|
||||||
|
FROM node:22-alpine
|
||||||
|
|
||||||
LABEL maintainer="Rishi Ghan <rishi.ghan@gmail.com>"
|
LABEL maintainer="Rishi Ghan <rishi.ghan@gmail.com>"
|
||||||
|
|
||||||
|
# Set the working directory inside the container
|
||||||
WORKDIR /threetwo
|
WORKDIR /threetwo
|
||||||
|
|
||||||
# Copy package.json and lock file first to leverage Docker cache
|
# Copy package.json and yarn.lock to leverage Docker cache
|
||||||
COPY package.json ./
|
COPY package.json yarn.lock ./
|
||||||
COPY yarn.lock ./
|
|
||||||
|
|
||||||
# Install build dependencies necessary for native modules
|
# Install build dependencies necessary for native modules (for node-sass)
|
||||||
RUN apk --no-cache add g++ make libpng-dev git python3 autoconf automake libjpeg-turbo-dev mesa-dev mesa libxi build-base gcc libtool nasm
|
RUN apk --no-cache add \
|
||||||
|
g++ \
|
||||||
|
make \
|
||||||
|
python3 \
|
||||||
|
autoconf \
|
||||||
|
automake \
|
||||||
|
libtool \
|
||||||
|
nasm \
|
||||||
|
git
|
||||||
|
|
||||||
# Install node modules
|
# Install node modules
|
||||||
RUN yarn install --ignore-engines
|
RUN yarn install --ignore-engines
|
||||||
|
|
||||||
# Explicitly install sass
|
# Explicitly install sass
|
||||||
RUN yarn add -D sass
|
RUN yarn add -D sass
|
||||||
# Copy the rest of the application
|
|
||||||
|
# Copy the rest of the application files into the container
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
# Expose the application port (default for Vite)
|
||||||
EXPOSE 5173
|
EXPOSE 5173
|
||||||
|
|
||||||
# Use yarn start if you want to stick with yarn, or change to npm start if you prefer npm
|
# Start the application with yarn
|
||||||
ENTRYPOINT [ "yarn", "start" ]
|
ENTRYPOINT ["yarn", "start"]
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"airdcpp-apisocket": "^2.5.0-beta.2",
|
"airdcpp-apisocket": "^2.5.0-beta.2",
|
||||||
"axios": "^1.7.4",
|
"axios": "^1.8.2",
|
||||||
"axios-cache-interceptor": "^1.0.1",
|
"axios-cache-interceptor": "^1.0.1",
|
||||||
"axios-rate-limit": "^1.3.0",
|
"axios-rate-limit": "^1.3.0",
|
||||||
"babel-plugin-styled-components": "^2.1.4",
|
"babel-plugin-styled-components": "^2.1.4",
|
||||||
@@ -41,6 +41,8 @@
|
|||||||
"final-form": "^4.20.2",
|
"final-form": "^4.20.2",
|
||||||
"final-form-arrays": "^3.0.2",
|
"final-form-arrays": "^3.0.2",
|
||||||
"focus-trap-react": "^10.2.3",
|
"focus-trap-react": "^10.2.3",
|
||||||
|
"graphql": "^16.0.0",
|
||||||
|
"graphql-request": "^7.2.0",
|
||||||
"history": "^5.3.0",
|
"history": "^5.3.0",
|
||||||
"html-to-text": "^8.1.0",
|
"html-to-text": "^8.1.0",
|
||||||
"i18next": "^23.11.1",
|
"i18next": "^23.11.1",
|
||||||
@@ -64,7 +66,7 @@
|
|||||||
"react-i18next": "^14.1.0",
|
"react-i18next": "^14.1.0",
|
||||||
"react-loader-spinner": "^4.0.0",
|
"react-loader-spinner": "^4.0.0",
|
||||||
"react-modal": "^3.15.1",
|
"react-modal": "^3.15.1",
|
||||||
"react-router": "^6.9.0",
|
"react-router": "^7.5.2",
|
||||||
"react-router-dom": "^6.9.0",
|
"react-router-dom": "^6.9.0",
|
||||||
"react-select": "^5.8.0",
|
"react-select": "^5.8.0",
|
||||||
"react-select-async-paginate": "^0.7.2",
|
"react-select-async-paginate": "^0.7.2",
|
||||||
@@ -74,7 +76,7 @@
|
|||||||
"socket.io-client": "^4.3.2",
|
"socket.io-client": "^4.3.2",
|
||||||
"styled-components": "^6.1.0",
|
"styled-components": "^6.1.0",
|
||||||
"threetwo-ui-typings": "^1.0.14",
|
"threetwo-ui-typings": "^1.0.14",
|
||||||
"vite": "^5.2.14",
|
"vite": "^5.4.19",
|
||||||
"vite-plugin-html": "^3.2.0",
|
"vite-plugin-html": "^3.2.0",
|
||||||
"websocket": "^1.0.34",
|
"websocket": "^1.0.34",
|
||||||
"zustand": "^4.4.6"
|
"zustand": "^4.4.6"
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import React, { ReactElement } from "react";
|
import React, { ReactElement, useEffect } from "react";
|
||||||
import { Outlet } from "react-router-dom";
|
import { Outlet } from "react-router-dom";
|
||||||
import { Navbar2 } from "./shared/Navbar2";
|
import { Navbar2 } from "./shared/Navbar2";
|
||||||
import { ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
import "../assets/scss/App.scss";
|
import "../assets/scss/App.scss";
|
||||||
|
import { useStore } from "../store";
|
||||||
|
|
||||||
export const App = (): ReactElement => {
|
export const App = (): ReactElement => {
|
||||||
|
useEffect(() => {
|
||||||
|
useStore.getState().getSocket("/"); // Connect to the base namespace
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Navbar2 />
|
<Navbar2 />
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import React, { useCallback, ReactElement, useEffect, useState } from "react";
|
import React, {
|
||||||
import { getBundlesForComic, sleep } from "../../actions/airdcpp.actions";
|
useCallback,
|
||||||
import { SearchQuery, PriorityEnum, SearchResponse } from "threetwo-ui-typings";
|
ReactElement,
|
||||||
import { RootState, SearchInstance } from "threetwo-ui-typings";
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { SearchQuery, PriorityEnum, SearchResponse, SearchInstance } from "threetwo-ui-typings";
|
||||||
import ellipsize from "ellipsize";
|
import ellipsize from "ellipsize";
|
||||||
import { Form, Field } from "react-final-form";
|
import { Form, Field } from "react-final-form";
|
||||||
import { difference } from "../../shared/utils/object.utils";
|
import { difference } from "../../shared/utils/object.utils";
|
||||||
@@ -10,60 +14,194 @@ import { useStore } from "../../store";
|
|||||||
import { useShallow } from "zustand/react/shallow";
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { AIRDCPP_SERVICE_BASE_URI } from "../../constants/endpoints";
|
import { AIRDCPP_SERVICE_BASE_URI, SETTINGS_SERVICE_BASE_URI } from "../../constants/endpoints";
|
||||||
|
import type { Socket } from "socket.io-client";
|
||||||
|
|
||||||
interface IAcquisitionPanelProps {
|
interface IAcquisitionPanelProps {
|
||||||
query: any;
|
query: any;
|
||||||
comicObjectId: any;
|
comicObjectId: string;
|
||||||
comicObject: any;
|
comicObject: any;
|
||||||
settings: any;
|
settings: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AirDCPPConfig {
|
||||||
|
protocol: string;
|
||||||
|
hostname: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SearchResult {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: {
|
||||||
|
id: string;
|
||||||
|
str: string;
|
||||||
|
};
|
||||||
|
size: number;
|
||||||
|
slots: {
|
||||||
|
total: number;
|
||||||
|
free: number;
|
||||||
|
};
|
||||||
|
users: {
|
||||||
|
user: {
|
||||||
|
nicks: string;
|
||||||
|
flags: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
dupe?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SearchInstanceData {
|
||||||
|
id: number;
|
||||||
|
owner: string;
|
||||||
|
expires_in: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SearchInfo {
|
||||||
|
query: {
|
||||||
|
pattern: string;
|
||||||
|
extensions: string[];
|
||||||
|
file_type: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Hub {
|
||||||
|
hub_url: string;
|
||||||
|
identity: {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SearchFormValues {
|
||||||
|
issueName: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const AcquisitionPanel = (
|
export const AcquisitionPanel = (
|
||||||
props: IAcquisitionPanelProps,
|
props: IAcquisitionPanelProps,
|
||||||
): ReactElement => {
|
): ReactElement => {
|
||||||
const { socketIOInstance } = useStore(
|
const socketRef = useRef<Socket>();
|
||||||
useShallow((state) => ({
|
const queryClient = useQueryClient();
|
||||||
socketIOInstance: state.socketIOInstance,
|
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
})),
|
|
||||||
);
|
|
||||||
|
|
||||||
interface SearchData {
|
const [dcppQuery, setDcppQuery] = useState<SearchQuery | null>(null);
|
||||||
query: Pick<SearchQuery, "pattern"> & Partial<Omit<SearchQuery, "pattern">>;
|
const [airDCPPSearchResults, setAirDCPPSearchResults] = useState<SearchResult[]>([]);
|
||||||
hub_urls: string[] | undefined | null;
|
const [airDCPPSearchStatus, setAirDCPPSearchStatus] = useState(false);
|
||||||
priority: PriorityEnum;
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
}
|
const [airDCPPSearchInstance, setAirDCPPSearchInstance] = useState<SearchInstanceData | null>(null);
|
||||||
interface SearchResult {
|
const [airDCPPSearchInfo, setAirDCPPSearchInfo] = useState<SearchInfo | null>(null);
|
||||||
result: {
|
const [searchError, setSearchError] = useState<string | null>(null);
|
||||||
id: number;
|
|
||||||
|
const { comicObjectId } = props;
|
||||||
|
const issueName = props.query.issue.name || "";
|
||||||
|
const sanitizedIssueName = issueName.replace(/[^a-zA-Z0-9 ]/g, " ");
|
||||||
|
|
||||||
|
// Search timeout duration in milliseconds (30 seconds)
|
||||||
|
const SEARCH_TIMEOUT_MS = 30000;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const socket = useStore.getState().getSocket("manual");
|
||||||
|
socketRef.current = socket;
|
||||||
|
|
||||||
|
// --- Handlers ---
|
||||||
|
const handleResultAdded = ({ result }: { result: SearchResult }) => {
|
||||||
|
setAirDCPPSearchResults((prev) =>
|
||||||
|
prev.some((r) => r.id === result.id) ? prev : [...prev, result],
|
||||||
|
);
|
||||||
};
|
};
|
||||||
search_id: number;
|
|
||||||
// Add other properties as needed
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSearch = (searchQuery) => {
|
const handleResultUpdated = ({ result }: { result: SearchResult }) => {
|
||||||
// Use the already connected socket instance to emit events
|
setAirDCPPSearchResults((prev) => {
|
||||||
socketIOInstance.emit("initiateSearch", searchQuery);
|
const idx = prev.findIndex((r) => r.id === result.id);
|
||||||
};
|
if (idx === -1) return prev;
|
||||||
|
if (JSON.stringify(prev[idx]) === JSON.stringify(result)) return prev;
|
||||||
|
const next = [...prev];
|
||||||
|
next[idx] = result;
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchInitiated = (data: { instance: SearchInstanceData }) => {
|
||||||
|
setAirDCPPSearchInstance(data.instance);
|
||||||
|
setIsSearching(true);
|
||||||
|
setSearchError(null);
|
||||||
|
|
||||||
|
// Clear any existing timeout
|
||||||
|
if (searchTimeoutRef.current) {
|
||||||
|
clearTimeout(searchTimeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a timeout to stop searching after SEARCH_TIMEOUT_MS
|
||||||
|
searchTimeoutRef.current = setTimeout(() => {
|
||||||
|
setIsSearching(false);
|
||||||
|
console.log(`Search timeout reached after ${SEARCH_TIMEOUT_MS / 1000} seconds`);
|
||||||
|
}, SEARCH_TIMEOUT_MS);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchesSent = (data: { searchInfo: SearchInfo }) => {
|
||||||
|
setAirDCPPSearchInfo(data.searchInfo);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchError = (error: { message: string }) => {
|
||||||
|
setSearchError(error.message || "Search failed");
|
||||||
|
setIsSearching(false);
|
||||||
|
|
||||||
|
// Clear timeout on error
|
||||||
|
if (searchTimeoutRef.current) {
|
||||||
|
clearTimeout(searchTimeoutRef.current);
|
||||||
|
searchTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchCompleted = () => {
|
||||||
|
setIsSearching(false);
|
||||||
|
|
||||||
|
// Clear timeout when search completes
|
||||||
|
if (searchTimeoutRef.current) {
|
||||||
|
clearTimeout(searchTimeoutRef.current);
|
||||||
|
searchTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Subscribe once ---
|
||||||
|
socket.on("searchResultAdded", handleResultAdded);
|
||||||
|
socket.on("searchResultUpdated", handleResultUpdated);
|
||||||
|
socket.on("searchInitiated", handleSearchInitiated);
|
||||||
|
socket.on("searchesSent", handleSearchesSent);
|
||||||
|
socket.on("searchError", handleSearchError);
|
||||||
|
socket.on("searchCompleted", handleSearchCompleted);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socket.off("searchResultAdded", handleResultAdded);
|
||||||
|
socket.off("searchResultUpdated", handleResultUpdated);
|
||||||
|
socket.off("searchInitiated", handleSearchInitiated);
|
||||||
|
socket.off("searchesSent", handleSearchesSent);
|
||||||
|
socket.off("searchError", handleSearchError);
|
||||||
|
socket.off("searchCompleted", handleSearchCompleted);
|
||||||
|
|
||||||
|
// Clean up timeout on unmount
|
||||||
|
if (searchTimeoutRef.current) {
|
||||||
|
clearTimeout(searchTimeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [SEARCH_TIMEOUT_MS]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: settings,
|
data: settings,
|
||||||
isLoading,
|
isLoading: isLoadingSettings,
|
||||||
isError,
|
isError: isSettingsError,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["settings"],
|
queryKey: ["settings"],
|
||||||
queryFn: async () =>
|
queryFn: async () =>
|
||||||
await axios({
|
await axios({
|
||||||
url: "http://localhost:3000/api/settings/getAllSettings",
|
url: `${SETTINGS_SERVICE_BASE_URI}/getAllSettings`,
|
||||||
method: "GET",
|
method: "GET",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
/**
|
|
||||||
* Get the hubs list from an AirDCPP Socket
|
const { data: hubs, isLoading: isLoadingHubs } = useQuery({
|
||||||
*/
|
queryKey: ["hubs", settings?.data.directConnect?.client?.host],
|
||||||
const { data: hubs } = useQuery({
|
|
||||||
queryKey: ["hubs"],
|
|
||||||
queryFn: async () =>
|
queryFn: async () =>
|
||||||
await axios({
|
await axios({
|
||||||
url: `${AIRDCPP_SERVICE_BASE_URI}/getHubs`,
|
url: `${AIRDCPP_SERVICE_BASE_URI}/getHubs`,
|
||||||
@@ -72,113 +210,72 @@ export const AcquisitionPanel = (
|
|||||||
host: settings?.data.directConnect?.client?.host,
|
host: settings?.data.directConnect?.client?.host,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
enabled: !isEmpty(settings?.data.directConnect?.client?.host),
|
enabled: !!settings?.data?.directConnect?.client?.host,
|
||||||
});
|
});
|
||||||
const { comicObjectId } = props;
|
|
||||||
const issueName = props.query.issue.name || "";
|
|
||||||
const sanitizedIssueName = issueName.replace(/[^a-zA-Z0-9 ]/g, " ");
|
|
||||||
|
|
||||||
const [dcppQuery, setDcppQuery] = useState({});
|
// Get AirDC++ config from settings
|
||||||
const [airDCPPSearchResults, setAirDCPPSearchResults] = useState<
|
const airDCPPConfig: AirDCPPConfig | null = settings?.data?.directConnect?.client
|
||||||
SearchResult[]
|
? {
|
||||||
>([]);
|
protocol: settings.data.directConnect.client.protocol || "ws",
|
||||||
const [airDCPPSearchStatus, setAirDCPPSearchStatus] = useState(false);
|
hostname: typeof settings.data.directConnect.client.host === 'string'
|
||||||
const [airDCPPSearchInstance, setAirDCPPSearchInstance] = useState({});
|
? settings.data.directConnect.client.host
|
||||||
const [airDCPPSearchInfo, setAirDCPPSearchInfo] = useState({});
|
: `${settings.data.directConnect.client.host?.hostname || 'localhost'}:${settings.data.directConnect.client.host?.port || '5600'}`,
|
||||||
const queryClient = useQueryClient();
|
username: settings.data.directConnect.client.username || "admin",
|
||||||
|
password: settings.data.directConnect.client.password || "password",
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
|
||||||
// Construct a AirDC++ query based on metadata inferred, upon component mount
|
|
||||||
// Pre-populate the search input with the search string, so that
|
|
||||||
// all the user has to do is hit "Search AirDC++" to perform a search
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// AirDC++ search query
|
if (hubs?.data && Array.isArray(hubs.data) && hubs.data.length > 0) {
|
||||||
const dcppSearchQuery = {
|
const dcppSearchQuery = {
|
||||||
query: {
|
query: {
|
||||||
pattern: `${sanitizedIssueName.replace(/#/g, "")}`,
|
pattern: `${sanitizedIssueName.replace(/#/g, "")}`,
|
||||||
extensions: ["cbz", "cbr", "cb7"],
|
extensions: ["cbz", "cbr", "cb7"],
|
||||||
},
|
|
||||||
hub_urls: map(hubs?.data, (item) => item.value),
|
|
||||||
priority: 5,
|
|
||||||
};
|
|
||||||
setDcppQuery(dcppSearchQuery);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to perform a search via an AirDC++ websocket
|
|
||||||
* @param {SearchData} data - a SearchData query
|
|
||||||
* @param {any} ADCPPSocket - an intialized AirDC++ socket instance
|
|
||||||
*/
|
|
||||||
const search = async (searchData: any) => {
|
|
||||||
setAirDCPPSearchResults([]);
|
|
||||||
socketIOInstance.emit(
|
|
||||||
"call",
|
|
||||||
"socket.search",
|
|
||||||
{
|
|
||||||
query: searchData,
|
|
||||||
config: {
|
|
||||||
protocol: `ws`,
|
|
||||||
hostname: `localhost:5600`,
|
|
||||||
username: `user`,
|
|
||||||
password: `pass`,
|
|
||||||
},
|
},
|
||||||
},
|
hub_urls: map(hubs.data, (item) => item.value),
|
||||||
(data: any) => console.log(data),
|
priority: 5,
|
||||||
);
|
};
|
||||||
|
setDcppQuery(dcppSearchQuery as any);
|
||||||
|
}
|
||||||
|
}, [hubs, sanitizedIssueName]);
|
||||||
|
|
||||||
|
const search = async (searchData: any) => {
|
||||||
|
if (!airDCPPConfig) {
|
||||||
|
setSearchError("AirDC++ configuration not found in settings");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!socketRef.current) {
|
||||||
|
setSearchError("Socket connection not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAirDCPPSearchResults([]);
|
||||||
|
setIsSearching(true);
|
||||||
|
setSearchError(null);
|
||||||
|
|
||||||
|
socketRef.current.emit("call", "socket.search", {
|
||||||
|
query: searchData,
|
||||||
|
namespace: "/manual",
|
||||||
|
config: airDCPPConfig,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
socketIOInstance.on("searchResultAdded", ({ groupedResult }: any) => {
|
|
||||||
setAirDCPPSearchResults((previousState) => {
|
|
||||||
const exists = previousState.some(
|
|
||||||
(item) => groupedResult.result.id === item.result.id,
|
|
||||||
);
|
|
||||||
if (!exists) {
|
|
||||||
return [...previousState, groupedResult];
|
|
||||||
}
|
|
||||||
return previousState;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
socketIOInstance.on("searchResultUpdated", ({ updatedResult }: any) => {
|
|
||||||
console.log("endh");
|
|
||||||
// ...update properties of the existing result in the UI
|
|
||||||
const bundleToUpdateIndex = airDCPPSearchResults?.findIndex(
|
|
||||||
(bundle) => bundle.result.id === updatedResult.result.id,
|
|
||||||
);
|
|
||||||
const updatedState = [...airDCPPSearchResults];
|
|
||||||
if (!isNil(difference(updatedState[bundleToUpdateIndex], updatedResult))) {
|
|
||||||
updatedState[bundleToUpdateIndex] = updatedResult;
|
|
||||||
}
|
|
||||||
setAirDCPPSearchResults((state) => [...state, ...updatedState]);
|
|
||||||
});
|
|
||||||
|
|
||||||
socketIOInstance.on("searchInitiated", (data) => {
|
|
||||||
setAirDCPPSearchInstance(data.instance);
|
|
||||||
});
|
|
||||||
socketIOInstance.on("searchesSent", (data) => {
|
|
||||||
setAirDCPPSearchInfo(data.searchInfo);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to download a bundle associated with a search result from AirDC++
|
|
||||||
* @param {Number} searchInstanceId - description
|
|
||||||
* @param {String} resultId - description
|
|
||||||
* @param {String} comicObjectId - description
|
|
||||||
* @param {String} name - description
|
|
||||||
* @param {Number} size - description
|
|
||||||
* @param {any} type - description
|
|
||||||
* @param {any} config - description
|
|
||||||
* @returns {void} - description
|
|
||||||
*/
|
|
||||||
const download = async (
|
const download = async (
|
||||||
searchInstanceId: Number,
|
searchInstanceId: number,
|
||||||
resultId: String,
|
resultId: string,
|
||||||
comicObjectId: String,
|
comicObjectId: string,
|
||||||
name: String,
|
name: string,
|
||||||
size: Number,
|
size: number,
|
||||||
type: any,
|
type: SearchResult["type"],
|
||||||
config: any,
|
config: AirDCPPConfig,
|
||||||
): void => {
|
): Promise<void> => {
|
||||||
socketIOInstance.emit(
|
if (!socketRef.current) {
|
||||||
|
console.error("Socket connection not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
socketRef.current.emit(
|
||||||
"call",
|
"call",
|
||||||
"socket.download",
|
"socket.download",
|
||||||
{
|
{
|
||||||
@@ -190,16 +287,27 @@ export const AcquisitionPanel = (
|
|||||||
type,
|
type,
|
||||||
config,
|
config,
|
||||||
},
|
},
|
||||||
(data: any) => console.log(data),
|
(data: any) => console.log("Download initiated:", data),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
const getDCPPSearchResults = async (searchQuery) => {
|
|
||||||
|
const getDCPPSearchResults = async (searchQuery: SearchFormValues) => {
|
||||||
|
if (!searchQuery.issueName || searchQuery.issueName.trim() === "") {
|
||||||
|
setSearchError("Please enter a search term");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hubs?.data || !Array.isArray(hubs.data) || hubs.data.length === 0) {
|
||||||
|
setSearchError("No hubs configured");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const manualQuery = {
|
const manualQuery = {
|
||||||
query: {
|
query: {
|
||||||
pattern: `${searchQuery.issueName}`,
|
pattern: `${searchQuery.issueName.trim()}`,
|
||||||
extensions: ["cbz", "cbr", "cb7"],
|
extensions: ["cbz", "cbr", "cb7"],
|
||||||
},
|
},
|
||||||
hub_urls: map(hubs?.data, (hub) => hub.hub_url),
|
hub_urls: [hubs.data[0].hub_url],
|
||||||
priority: 5,
|
priority: 5,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -208,8 +316,13 @@ export const AcquisitionPanel = (
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mt-5">
|
<div className="mt-5 mb-3">
|
||||||
{!isEmpty(hubs?.data) ? (
|
{isLoadingSettings || isLoadingHubs ? (
|
||||||
|
<div className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
<i className="icon-[solar--refresh-bold-duotone] h-5 w-5 animate-spin" />
|
||||||
|
Loading configuration...
|
||||||
|
</div>
|
||||||
|
) : !isEmpty(hubs?.data) ? (
|
||||||
<Form
|
<Form
|
||||||
onSubmit={getDCPPSearchResults}
|
onSubmit={getDCPPSearchResults}
|
||||||
initialValues={{
|
initialValues={{
|
||||||
@@ -229,20 +342,31 @@ export const AcquisitionPanel = (
|
|||||||
{...input}
|
{...input}
|
||||||
className="dark:bg-slate-400 bg-slate-300 py-2 px-2 rounded-l-md border-gray-300 h-10 min-w-full dark:text-slate-800 sm:text-md sm:leading-5 focus:outline-none focus:shadow-outline-blue focus:border-blue-300"
|
className="dark:bg-slate-400 bg-slate-300 py-2 px-2 rounded-l-md border-gray-300 h-10 min-w-full dark:text-slate-800 sm:text-md sm:leading-5 focus:outline-none focus:shadow-outline-blue focus:border-blue-300"
|
||||||
placeholder="Type an issue/volume name"
|
placeholder="Type an issue/volume name"
|
||||||
|
disabled={isSearching}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="sm:mt-0 min-w-fit rounded-r-lg border border-green-400 dark:border-green-200 bg-green-200 px-3 py-1 text-gray-500 hover:bg-transparent hover:text-green-600 focus:outline-none focus:ring active:text-indigo-500"
|
className="sm:mt-0 min-w-fit rounded-r-lg border border-green-400 dark:border-green-200 bg-green-200 px-3 py-1 text-gray-500 hover:bg-transparent hover:text-green-600 focus:outline-none focus:ring active:text-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
disabled={isSearching}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row items-center">
|
||||||
Search DC++
|
{isSearching ? (
|
||||||
<div className="h-5 w-5 ml-2">
|
<>
|
||||||
<img
|
<i className="icon-[solar--refresh-bold-duotone] h-5 w-5 animate-spin mr-2" />
|
||||||
src="/src/client/assets/img/airdcpp_logo.svg"
|
Searching...
|
||||||
className="h-5 w-5"
|
</>
|
||||||
/>
|
) : (
|
||||||
</div>
|
<>
|
||||||
|
Search DC++
|
||||||
|
<div className="h-5 w-5 ml-2">
|
||||||
|
<img
|
||||||
|
src="/src/client/assets/img/airdcpp_logo.svg"
|
||||||
|
className="h-5 w-5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -254,27 +378,45 @@ export const AcquisitionPanel = (
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="">
|
<article
|
||||||
<article className="">
|
role="alert"
|
||||||
<div className="">
|
className="mt-4 rounded-lg text-sm max-w-screen-md border-s-4 border-yellow-500 bg-yellow-50 p-4 dark:border-s-4 dark:border-yellow-600 dark:bg-yellow-300 dark:text-slate-600"
|
||||||
No AirDC++ hub configured. Please configure it in{" "}
|
>
|
||||||
<code>Settings > AirDC++ > Hubs</code>.
|
No AirDC++ hub configured. Please configure it in{" "}
|
||||||
</div>
|
<code>Settings > AirDC++ > Hubs</code>.
|
||||||
</article>
|
</article>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Search Error Display */}
|
||||||
|
{searchError && (
|
||||||
|
<article
|
||||||
|
role="alert"
|
||||||
|
className="mt-4 rounded-lg text-sm max-w-screen-md border-s-4 border-red-500 bg-red-50 p-4 dark:border-s-4 dark:border-red-600 dark:bg-red-300 dark:text-slate-600"
|
||||||
|
>
|
||||||
|
<strong>Error:</strong> {searchError}
|
||||||
|
</article>
|
||||||
|
)}
|
||||||
|
{/* configured hub */}
|
||||||
|
{!isEmpty(hubs?.data) && hubs?.data[0] && (
|
||||||
|
<span className="inline-flex items-center bg-green-50 text-slate-800 text-xs font-medium px-2.5 py-0.5 rounded-md dark:text-slate-900 dark:bg-green-300">
|
||||||
|
<span className="pr-1 pt-1">
|
||||||
|
<i className="icon-[solar--server-2-bold-duotone] w-5 h-5"></i>
|
||||||
|
</span>
|
||||||
|
{hubs.data[0].hub_url}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* AirDC++ search instance details */}
|
{/* AirDC++ search instance details */}
|
||||||
{!isNil(airDCPPSearchInstance) &&
|
{airDCPPSearchInstance &&
|
||||||
!isEmpty(airDCPPSearchInfo) &&
|
airDCPPSearchInfo &&
|
||||||
!isNil(hubs) && (
|
hubs?.data && (
|
||||||
<div className="flex flex-row gap-3 my-5 font-hasklig">
|
<div className="flex flex-row gap-3 my-5 font-hasklig">
|
||||||
<div className="block max-w-sm h-fit p-6 text-sm bg-white border border-gray-200 rounded-lg shadow dark:bg-slate-400 dark:border-gray-700">
|
<div className="block max-w-sm h-fit p-6 text-sm bg-white border border-gray-200 rounded-lg shadow dark:bg-slate-400 dark:border-gray-700">
|
||||||
<dl>
|
<dl>
|
||||||
<dt>
|
<dt>
|
||||||
<div className="mb-1">
|
<div className="mb-1">
|
||||||
{hubs?.data.map((value, idx) => (
|
{hubs.data.map((value: Hub, idx: number) => (
|
||||||
<span className="tag is-warning" key={idx}>
|
<span className="tag is-warning" key={idx}>
|
||||||
{value.identity.name}
|
{value.identity.name}
|
||||||
</span>
|
</span>
|
||||||
@@ -313,143 +455,119 @@ export const AcquisitionPanel = (
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* AirDC++ results */}
|
{/* AirDC++ results */}
|
||||||
<div className="columns">
|
<div className="">
|
||||||
{!isNil(airDCPPSearchResults) && !isEmpty(airDCPPSearchResults) ? (
|
{airDCPPSearchResults.length > 0 ? (
|
||||||
<div className="overflow-x-auto w-fit mt-4 rounded-lg border border-gray-200 dark:border-gray-500">
|
<div className="overflow-x-auto max-w-full mt-6">
|
||||||
<table className="min-w-full divide-y-2 divide-gray-200 dark:divide-gray-500 text-md">
|
<table className="w-full table-auto text-sm text-gray-900 dark:text-slate-100">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr className="border-b border-gray-300 dark:border-slate-700">
|
||||||
<th className="whitespace-nowrap px-2 py-2 font-medium text-gray-900 dark:text-slate-200">
|
<th className="whitespace-nowrap px-3 py-2 text-left text-[11px] font-semibold tracking-wide text-gray-500 dark:text-slate-400 uppercase">
|
||||||
Name
|
Name
|
||||||
</th>
|
</th>
|
||||||
<th className="whitespace-nowrap py-2 font-medium text-gray-900 dark:text-slate-200">
|
<th className="whitespace-nowrap px-3 py-2 text-left text-[11px] font-semibold tracking-wide text-gray-500 dark:text-slate-400 uppercase">
|
||||||
Type
|
Type
|
||||||
</th>
|
</th>
|
||||||
<th className="whitespace-nowrap py-2 font-medium text-gray-900 dark:text-slate-200">
|
<th className="whitespace-nowrap px-3 py-2 text-left text-[11px] font-semibold tracking-wide text-gray-500 dark:text-slate-400 uppercase">
|
||||||
Slots
|
Slots
|
||||||
</th>
|
</th>
|
||||||
<th className="whitespace-nowrap py-2 font-medium text-gray-900 dark:text-slate-200">
|
<th className="whitespace-nowrap px-3 py-2 text-left text-[11px] font-semibold tracking-wide text-gray-500 dark:text-slate-400 uppercase">
|
||||||
Actions
|
Actions
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-100 dark:divide-gray-500">
|
<tbody>
|
||||||
{map(airDCPPSearchResults, ({ result, search_id }, idx) => {
|
{map(
|
||||||
return (
|
airDCPPSearchResults,
|
||||||
|
({ dupe, type, name, id, slots, users, size }, idx) => (
|
||||||
<tr
|
<tr
|
||||||
key={idx}
|
key={idx}
|
||||||
className={
|
className={
|
||||||
!isNil(result.dupe)
|
!isNil(dupe)
|
||||||
? "bg-gray-100 dark:bg-gray-700"
|
? "border-b border-gray-200 dark:border-slate-700 bg-gray-100 dark:bg-gray-700"
|
||||||
: "w-fit text-sm"
|
: "border-b border-gray-200 dark:border-slate-700 text-sm"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<td className="whitespace-nowrap px-3 py-3 text-gray-700 dark:text-slate-300">
|
{/* NAME */}
|
||||||
|
<td className="whitespace-nowrap px-3 py-3 text-gray-700 dark:text-slate-300 max-w-xs">
|
||||||
<p className="mb-2">
|
<p className="mb-2">
|
||||||
{result.type.id === "directory" ? (
|
{type.id === "directory" && (
|
||||||
<i className="fas fa-folder"></i>
|
<i className="fas fa-folder mr-1"></i>
|
||||||
) : null}
|
)}
|
||||||
{ellipsize(result.name, 70)}
|
{ellipsize(name, 45)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<dl>
|
<dl>
|
||||||
<dd>
|
<dd>
|
||||||
<div className="inline-flex flex-row gap-2">
|
<div className="inline-flex flex-wrap gap-1">
|
||||||
{!isNil(result.dupe) ? (
|
{!isNil(dupe) && (
|
||||||
<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="inline-flex items-center gap-1 bg-slate-100 text-slate-800 text-xs font-medium py-0.5 px-2 rounded dark:bg-slate-400 dark:text-slate-900">
|
||||||
<span className="pr-1 pt-1">
|
<i className="icon-[solar--copy-bold-duotone] w-4 h-4"></i>
|
||||||
<i className="icon-[solar--copy-bold-duotone] w-5 h-5"></i>
|
Dupe
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="text-md text-slate-500 dark:text-slate-900">
|
|
||||||
Dupe
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{/* Nicks */}
|
|
||||||
<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--user-rounded-bold-duotone] w-5 h-5"></i>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="text-md text-slate-500 dark:text-slate-900">
|
|
||||||
{result.users.user.nicks}
|
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="inline-flex items-center gap-1 bg-slate-100 text-slate-800 text-xs font-medium py-0.5 px-2 rounded dark:bg-slate-400 dark:text-slate-900">
|
||||||
|
<i className="icon-[solar--user-rounded-bold-duotone] w-4 h-4"></i>
|
||||||
|
{users.user.nicks}
|
||||||
</span>
|
</span>
|
||||||
{/* Flags */}
|
{users.user.flags.map((flag: string, flagIdx: number) => (
|
||||||
{result.users.user.flags.map((flag, idx) => (
|
<span
|
||||||
<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">
|
key={flagIdx}
|
||||||
<span className="pr-1 pt-1">
|
className="inline-flex items-center gap-1 bg-slate-100 text-slate-800 text-xs font-medium py-0.5 px-2 rounded dark:bg-slate-400 dark:text-slate-900"
|
||||||
<i className="icon-[solar--tag-horizontal-bold-duotone] w-5 h-5"></i>
|
>
|
||||||
</span>
|
<i className="icon-[solar--tag-horizontal-bold-duotone] w-4 h-4"></i>
|
||||||
|
{flag}
|
||||||
<span className="text-md text-slate-500 dark:text-slate-900">
|
|
||||||
{flag}
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
|
||||||
{/* Extension */}
|
|
||||||
<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--zip-file-bold-duotone] w-5 h-5"></i>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="text-md text-slate-500 dark:text-slate-900">
|
{/* TYPE */}
|
||||||
{result.type.str}
|
<td className="px-2 py-3">
|
||||||
</span>
|
<span className="inline-flex items-center gap-1 bg-slate-100 text-slate-800 text-xs font-medium py-0.5 px-2 rounded dark:bg-slate-400 dark:text-slate-900">
|
||||||
|
<i className="icon-[solar--zip-file-bold-duotone] w-4 h-4"></i>
|
||||||
|
{type.str}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-2">
|
|
||||||
{/* Slots */}
|
|
||||||
<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--settings-minimalistic-bold-duotone] w-5 h-5"></i>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="text-md text-slate-500 dark:text-slate-900">
|
{/* SLOTS */}
|
||||||
{result.slots.total} slots; {result.slots.free} free
|
<td className="px-2 py-3">
|
||||||
</span>
|
<span className="inline-flex items-center gap-1 bg-slate-100 text-slate-800 text-xs font-medium py-0.5 px-2 rounded dark:bg-slate-400 dark:text-slate-900">
|
||||||
|
<i className="icon-[solar--settings-minimalistic-bold-duotone] w-4 h-4"></i>
|
||||||
|
{slots.total} slots; {slots.free} free
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-2">
|
|
||||||
|
{/* ACTIONS */}
|
||||||
|
<td className="px-2 py-3">
|
||||||
<button
|
<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-3 py-1 text-gray-500 hover:bg-transparent hover:text-green-600 focus:outline-none focus:ring active:text-indigo-500"
|
className="inline-flex items-center gap-1 rounded border border-green-500 bg-green-500 px-2 py-1 text-xs font-medium text-white hover:bg-transparent hover:text-green-400 dark:border-green-300 dark:bg-green-300 dark:text-slate-900 dark:hover:bg-transparent disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
onClick={() =>
|
onClick={() => {
|
||||||
download(
|
if (airDCPPSearchInstance && airDCPPConfig) {
|
||||||
airDCPPSearchInstance.id,
|
download(
|
||||||
result.id,
|
airDCPPSearchInstance.id,
|
||||||
comicObjectId,
|
id,
|
||||||
result.name,
|
comicObjectId,
|
||||||
result.size,
|
name,
|
||||||
result.type,
|
size,
|
||||||
{
|
type,
|
||||||
protocol: `ws`,
|
airDCPPConfig,
|
||||||
hostname: `localhost:5600`,
|
);
|
||||||
username: `user`,
|
}
|
||||||
password: `pass`,
|
}}
|
||||||
},
|
disabled={!airDCPPSearchInstance || !airDCPPConfig}
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<span className="text-xs">Download</span>
|
Download
|
||||||
<span className="w-5 h-5">
|
<i className="icon-[solar--download-bold-duotone] w-4 h-4"></i>
|
||||||
<i className="h-5 w-5 icon-[solar--download-bold-duotone]"></i>
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
),
|
||||||
})}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : !isSearching ? (
|
||||||
<div className="">
|
<div className="">
|
||||||
<article
|
<article
|
||||||
role="alert"
|
role="alert"
|
||||||
@@ -475,6 +593,11 @@ export const AcquisitionPanel = (
|
|||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center gap-2 text-sm text-gray-600 dark:text-gray-400 mt-6 p-4">
|
||||||
|
<i className="icon-[solar--refresh-bold-duotone] h-6 w-6 animate-spin" />
|
||||||
|
Searching...
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { ReactElement } from "react";
|
import React, { ReactElement } from "react";
|
||||||
import Select from "react-select";
|
import Select from "react-select";
|
||||||
|
|
||||||
export const Menu = (props): ReactElement => {
|
export const Menu = (props: any): ReactElement => {
|
||||||
const {
|
const {
|
||||||
filteredActionOptions,
|
filteredActionOptions,
|
||||||
customStyles,
|
customStyles,
|
||||||
@@ -13,11 +13,11 @@ export const Menu = (props): ReactElement => {
|
|||||||
<Select
|
<Select
|
||||||
components={{ Placeholder }}
|
components={{ Placeholder }}
|
||||||
placeholder={
|
placeholder={
|
||||||
<span className="inline-flex flex-row items-center gap-2 pt-1">
|
<span className="inline-flex flex-row items-center gap-1.5 pt-1">
|
||||||
<div className="w-6 h-6">
|
<div className="w-4 h-4">
|
||||||
<i className="icon-[solar--cursor-bold-duotone] w-6 h-6"></i>
|
<i className="icon-[solar--cursor-bold-duotone] w-4 h-4"></i>
|
||||||
</div>
|
</div>
|
||||||
<div>Select An Action</div>
|
<div className="text-sm">Select An Action</div>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
styles={customStyles}
|
styles={customStyles}
|
||||||
|
|||||||
@@ -3,44 +3,55 @@ import prettyBytes from "pretty-bytes";
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import ellipsize from "ellipsize";
|
import ellipsize from "ellipsize";
|
||||||
import { map } from "lodash";
|
import { map } from "lodash";
|
||||||
|
import { DownloadProgressTick } from "./DownloadProgressTick";
|
||||||
export const AirDCPPBundles = (props) => {
|
export const AirDCPPBundles = (props) => {
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto w-fit mt-4 rounded-lg border border-gray-200">
|
<div className="overflow-x-auto w-fit mt-6">
|
||||||
<table className="min-w-full divide-y-2 divide-gray-200 dark:divide-gray-200 text-md">
|
<table className="min-w-full text-sm text-gray-900 dark:text-slate-100">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr className="border-b border-gray-300 dark:border-slate-700">
|
||||||
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
|
<th className="px-3 py-2 text-left text-[11px] font-semibold tracking-wide text-gray-500 dark:text-slate-400 uppercase">
|
||||||
Filename
|
Filename
|
||||||
</th>
|
</th>
|
||||||
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
|
<th className="px-3 py-2 text-left text-[11px] font-semibold tracking-wide text-gray-500 dark:text-slate-400 uppercase">
|
||||||
Size
|
Size
|
||||||
</th>
|
</th>
|
||||||
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
|
<th className="px-3 py-2 text-left text-[11px] font-semibold tracking-wide text-gray-500 dark:text-slate-400 uppercase">
|
||||||
Download Time
|
Download Status
|
||||||
</th>
|
</th>
|
||||||
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
|
<th className="px-3 py-2 text-left text-[11px] font-semibold tracking-wide text-gray-500 dark:text-slate-400 uppercase">
|
||||||
Bundle ID
|
Bundle ID
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-200">
|
<tbody>
|
||||||
{map(props.data, (bundle) => (
|
{map(props.data, (bundle, index) => (
|
||||||
<tr key={bundle.id} className="text-sm">
|
<tr
|
||||||
<td className="whitespace-nowrap px-2 py-2 text-gray-700 dark:text-slate-300">
|
key={bundle.id}
|
||||||
<h5>{ellipsize(bundle.name, 58)}</h5>
|
className={
|
||||||
<span className="text-xs">{ellipsize(bundle.target, 88)}</span>
|
Number(index) !== props.data.length - 1
|
||||||
|
? "border-b border-gray-200 dark:border-slate-700"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<td className="px-3 py-2 align-top">
|
||||||
|
<h5 className="font-medium text-gray-800 dark:text-slate-200">
|
||||||
|
{ellipsize(bundle.name, 58)}
|
||||||
|
</h5>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-slate-400">
|
||||||
|
{ellipsize(bundle.target, 88)}
|
||||||
|
</p>
|
||||||
</td>
|
</td>
|
||||||
<td className="whitespace-nowrap px-2 py-2 text-gray-700 dark:text-slate-300">
|
<td className="px-3 py-2 align-top">
|
||||||
{prettyBytes(bundle.size)}
|
{prettyBytes(bundle.size)}
|
||||||
</td>
|
</td>
|
||||||
<td className="whitespace-nowrap px-2 py-2 text-gray-700 dark:text-slate-300">
|
<td className="px-3 py-2 align-top">
|
||||||
{dayjs
|
<DownloadProgressTick bundleId={bundle.id} />
|
||||||
.unix(bundle.time_finished)
|
|
||||||
.format("h:mm on ddd, D MMM, YYYY")}
|
|
||||||
</td>
|
</td>
|
||||||
<td className="whitespace-nowrap px-2 py-2 text-gray-700 dark:text-slate-300">
|
<td className="px-3 py-2 align-top">
|
||||||
<span className="tag is-warning">{bundle.id}</span>
|
<span className="text-xs text-yellow-800 dark:text-yellow-300 font-medium">
|
||||||
|
{bundle.id}
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import React, { ReactElement, useCallback, useState } from "react";
|
import React, { ReactElement, useCallback, useState } from "react";
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import { fetchMetronResource } from "../../../actions/metron.actions";
|
import { fetchMetronResource } from "../../../actions/metron.actions";
|
||||||
import Creatable from "react-select/creatable";
|
import Creatable from "react-select/creatable";
|
||||||
import { withAsyncPaginate } from "react-select-async-paginate";
|
import { withAsyncPaginate } from "react-select-async-paginate";
|
||||||
const CreatableAsyncPaginate = withAsyncPaginate(Creatable);
|
const CreatableAsyncPaginate = withAsyncPaginate(Creatable);
|
||||||
|
|
||||||
export const AsyncSelectPaginate = (props): ReactElement => {
|
interface AsyncSelectPaginateProps {
|
||||||
|
metronResource: string;
|
||||||
|
placeholder?: string;
|
||||||
|
value?: object;
|
||||||
|
onChange?(...args: unknown[]): unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AsyncSelectPaginate = (props: AsyncSelectPaginateProps): ReactElement => {
|
||||||
const [value, setValue] = useState(null);
|
const [value, setValue] = useState(null);
|
||||||
const [isAddingInProgress, setIsAddingInProgress] = useState(false);
|
const [isAddingInProgress, setIsAddingInProgress] = useState(false);
|
||||||
|
|
||||||
@@ -38,11 +44,4 @@ export const AsyncSelectPaginate = (props): ReactElement => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
AsyncSelectPaginate.propTypes = {
|
|
||||||
metronResource: PropTypes.string.isRequired,
|
|
||||||
placeholder: PropTypes.string,
|
|
||||||
value: PropTypes.object,
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AsyncSelectPaginate;
|
export default AsyncSelectPaginate;
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import { VolumeInformation } from "./Tabs/VolumeInformation";
|
|||||||
|
|
||||||
import { isEmpty, isUndefined, isNil, filter } from "lodash";
|
import { isEmpty, isUndefined, isNil, filter } from "lodash";
|
||||||
import { components } from "react-select";
|
import { components } from "react-select";
|
||||||
import { RootState } from "threetwo-ui-typings";
|
|
||||||
|
|
||||||
import "react-sliding-pane/dist/react-sliding-pane.css";
|
import "react-sliding-pane/dist/react-sliding-pane.css";
|
||||||
import "react-loader-spinner/dist/loader/css/react-spinner-loader.css";
|
import "react-loader-spinner/dist/loader/css/react-spinner-loader.css";
|
||||||
@@ -34,7 +33,34 @@ import { styled } from "styled-components";
|
|||||||
import { COMICVINE_SERVICE_URI } from "../../constants/endpoints";
|
import { COMICVINE_SERVICE_URI } from "../../constants/endpoints";
|
||||||
import { refineQuery } from "filename-parser";
|
import { refineQuery } from "filename-parser";
|
||||||
|
|
||||||
type ComicDetailProps = {};
|
interface ComicDetailProps {
|
||||||
|
data: {
|
||||||
|
_id: string;
|
||||||
|
rawFileDetails?: any;
|
||||||
|
inferredMetadata?: {
|
||||||
|
issue?: {
|
||||||
|
year?: string;
|
||||||
|
name?: string;
|
||||||
|
number?: number;
|
||||||
|
subtitle?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
sourcedMetadata: {
|
||||||
|
comicvine?: any;
|
||||||
|
locg?: any;
|
||||||
|
comicInfo?: any;
|
||||||
|
};
|
||||||
|
acquisition?: {
|
||||||
|
directconnect?: {
|
||||||
|
downloads?: any[];
|
||||||
|
};
|
||||||
|
torrent?: any[];
|
||||||
|
};
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
userSettings?: any;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Component for displaying the metadata for a comic in greater detail.
|
* Component for displaying the metadata for a comic in greater detail.
|
||||||
*
|
*
|
||||||
@@ -68,7 +94,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
|||||||
|
|
||||||
// const dispatch = useDispatch();
|
// const dispatch = useDispatch();
|
||||||
|
|
||||||
const openModal = useCallback((filePath) => {
|
const openModal = useCallback((filePath: string) => {
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
// dispatch(
|
// dispatch(
|
||||||
// extractComicArchive(filePath, {
|
// extractComicArchive(filePath, {
|
||||||
@@ -85,7 +111,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
|||||||
const StyledSlidingPanel = styled(SlidingPane)`
|
const StyledSlidingPanel = styled(SlidingPane)`
|
||||||
background: #ccc;
|
background: #ccc;
|
||||||
`;
|
`;
|
||||||
const afterOpenModal = useCallback((things) => {
|
const afterOpenModal = useCallback((things: any) => {
|
||||||
// references are now sync'd and can be accessed.
|
// references are now sync'd and can be accessed.
|
||||||
// subtitle.style.color = "#f00";
|
// subtitle.style.color = "#f00";
|
||||||
console.log("kolaveri", things);
|
console.log("kolaveri", things);
|
||||||
@@ -96,9 +122,9 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// sliding panel init
|
// sliding panel init
|
||||||
const contentForSlidingPanel = {
|
const contentForSlidingPanel: Record<string, { content: (props?: any) => JSX.Element }> = {
|
||||||
CVMatches: {
|
CVMatches: {
|
||||||
content: (props) => (
|
content: (props?: any) => (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<ComicVineSearchForm data={rawFileDetails} />
|
<ComicVineSearchForm data={rawFileDetails} />
|
||||||
@@ -106,7 +132,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
|||||||
|
|
||||||
<div className="border-slate-500 border rounded-lg p-2 mt-3">
|
<div className="border-slate-500 border rounded-lg p-2 mt-3">
|
||||||
<p className="">Searching for:</p>
|
<p className="">Searching for:</p>
|
||||||
{inferredMetadata.issue ? (
|
{inferredMetadata?.issue ? (
|
||||||
<>
|
<>
|
||||||
<span className="">{inferredMetadata.issue.name} </span>
|
<span className="">{inferredMetadata.issue.name} </span>
|
||||||
<span className=""> # {inferredMetadata.issue.number} </span>
|
<span className=""> # {inferredMetadata.issue.number} </span>
|
||||||
@@ -131,9 +157,9 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
|||||||
// Actions
|
// Actions
|
||||||
|
|
||||||
const fetchComicVineMatches = async (
|
const fetchComicVineMatches = async (
|
||||||
searchPayload,
|
searchPayload: any,
|
||||||
issueSearchQuery,
|
issueSearchQuery: any,
|
||||||
seriesSearchQuery,
|
seriesSearchQuery: any,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios({
|
const response = await axios({
|
||||||
@@ -153,7 +179,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
|||||||
},
|
},
|
||||||
rawFileDetails: searchPayload,
|
rawFileDetails: searchPayload,
|
||||||
},
|
},
|
||||||
transformResponse: (r) => {
|
transformResponse: (r: string) => {
|
||||||
const matches = JSON.parse(r);
|
const matches = JSON.parse(r);
|
||||||
return matches;
|
return matches;
|
||||||
// return sortBy(matches, (match) => -match.score);
|
// return sortBy(matches, (match) => -match.score);
|
||||||
@@ -163,9 +189,9 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
|||||||
if (!isNil(response.data.results) && response.data.results.length === 1) {
|
if (!isNil(response.data.results) && response.data.results.length === 1) {
|
||||||
matches = response.data.results;
|
matches = response.data.results;
|
||||||
} else {
|
} else {
|
||||||
matches = response.data.map((match) => match);
|
matches = response.data.map((match: any) => match);
|
||||||
}
|
}
|
||||||
const scoredMatches = matches.sort((a, b) => b.score - a.score);
|
const scoredMatches = matches.sort((a: any, b: any) => b.score - a.score);
|
||||||
setComicVineMatches(scoredMatches);
|
setComicVineMatches(scoredMatches);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
@@ -174,13 +200,13 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
|||||||
|
|
||||||
// Action event handlers
|
// Action event handlers
|
||||||
const openDrawerWithCVMatches = () => {
|
const openDrawerWithCVMatches = () => {
|
||||||
let seriesSearchQuery: IComicVineSearchQuery = {} as IComicVineSearchQuery;
|
let seriesSearchQuery: any = {};
|
||||||
let issueSearchQuery: IComicVineSearchQuery = {} as IComicVineSearchQuery;
|
let issueSearchQuery: any = {};
|
||||||
|
|
||||||
if (!isUndefined(rawFileDetails)) {
|
if (!isUndefined(rawFileDetails)) {
|
||||||
issueSearchQuery = refineQuery(rawFileDetails.name);
|
issueSearchQuery = refineQuery((rawFileDetails as any).name);
|
||||||
} else if (!isEmpty(comicvine)) {
|
} else if (!isEmpty(comicvine)) {
|
||||||
issueSearchQuery = refineQuery(comicvine.name);
|
issueSearchQuery = refineQuery((comicvine as any).name);
|
||||||
}
|
}
|
||||||
fetchComicVineMatches(rawFileDetails, issueSearchQuery, seriesSearchQuery);
|
fetchComicVineMatches(rawFileDetails, issueSearchQuery, seriesSearchQuery);
|
||||||
setSlidingPanelContentId("CVMatches");
|
setSlidingPanelContentId("CVMatches");
|
||||||
@@ -194,30 +220,30 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
|||||||
|
|
||||||
// Actions menu options and handler
|
// Actions menu options and handler
|
||||||
const CVMatchLabel = (
|
const CVMatchLabel = (
|
||||||
<span className="inline-flex flex-row items-center gap-2">
|
<span className="inline-flex flex-row items-center gap-1.5">
|
||||||
<div className="w-6 h-6">
|
<div className="w-4 h-4">
|
||||||
<i className="icon-[solar--magic-stick-3-bold-duotone] w-6 h-6"></i>
|
<i className="icon-[solar--magic-stick-3-bold-duotone] w-4 h-4"></i>
|
||||||
</div>
|
</div>
|
||||||
<div>Match on ComicVine</div>
|
<div className="text-sm">Match on ComicVine</div>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
const editLabel = (
|
const editLabel = (
|
||||||
<span className="inline-flex flex-row items-center gap-2">
|
<span className="inline-flex flex-row items-center gap-1.5">
|
||||||
<div className="w-6 h-6">
|
<div className="w-4 h-4">
|
||||||
<i className="icon-[solar--pen-2-bold-duotone] w-6 h-6"></i>
|
<i className="icon-[solar--pen-2-bold-duotone] w-4 h-4"></i>
|
||||||
</div>
|
</div>
|
||||||
<div>Edit Metadata</div>
|
<div className="text-sm">Edit Metadata</div>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
const deleteLabel = (
|
const deleteLabel = (
|
||||||
<span className="inline-flex flex-row items-center gap-2">
|
<span className="inline-flex flex-row items-center gap-1.5">
|
||||||
<div className="w-6 h-6">
|
<div className="w-4 h-4">
|
||||||
<i className="icon-[solar--trash-bin-trash-bold-duotone] w-6 h-6"></i>
|
<i className="icon-[solar--trash-bin-trash-bold-duotone] w-4 h-4"></i>
|
||||||
</div>
|
</div>
|
||||||
<div>Delete Comic</div>
|
<div className="text-sm">Delete Comic</div>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
const Placeholder = (props) => {
|
const Placeholder = (props: any) => {
|
||||||
return <components.Placeholder {...props} />;
|
return <components.Placeholder {...props} />;
|
||||||
};
|
};
|
||||||
const actionOptions = [
|
const actionOptions = [
|
||||||
@@ -232,7 +258,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
|||||||
}
|
}
|
||||||
return item;
|
return item;
|
||||||
});
|
});
|
||||||
const handleActionSelection = (action) => {
|
const handleActionSelection = (action: any) => {
|
||||||
switch (action.value) {
|
switch (action.value) {
|
||||||
case "match-on-comic-vine":
|
case "match-on-comic-vine":
|
||||||
openDrawerWithCVMatches();
|
openDrawerWithCVMatches();
|
||||||
@@ -246,23 +272,23 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const customStyles = {
|
const customStyles = {
|
||||||
menu: (base) => ({
|
menu: (base: any) => ({
|
||||||
...base,
|
...base,
|
||||||
backgroundColor: "rgb(156, 163, 175)",
|
backgroundColor: "rgb(156, 163, 175)",
|
||||||
}),
|
}),
|
||||||
placeholder: (base) => ({
|
placeholder: (base: any) => ({
|
||||||
...base,
|
...base,
|
||||||
color: "black",
|
color: "black",
|
||||||
}),
|
}),
|
||||||
option: (base, { data, isDisabled, isFocused, isSelected }) => ({
|
option: (base: any, { data, isDisabled, isFocused, isSelected }: any) => ({
|
||||||
...base,
|
...base,
|
||||||
backgroundColor: isFocused ? "gray" : "rgb(156, 163, 175)",
|
backgroundColor: isFocused ? "gray" : "rgb(156, 163, 175)",
|
||||||
}),
|
}),
|
||||||
singleValue: (base) => ({
|
singleValue: (base: any) => ({
|
||||||
...base,
|
...base,
|
||||||
paddingTop: "0.4rem",
|
paddingTop: "0.4rem",
|
||||||
}),
|
}),
|
||||||
control: (base) => ({
|
control: (base: any) => ({
|
||||||
...base,
|
...base,
|
||||||
backgroundColor: "rgb(156, 163, 175)",
|
backgroundColor: "rgb(156, 163, 175)",
|
||||||
color: "black",
|
color: "black",
|
||||||
@@ -272,7 +298,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
|||||||
|
|
||||||
// check for the availability of CV metadata
|
// check for the availability of CV metadata
|
||||||
const isComicBookMetadataAvailable =
|
const isComicBookMetadataAvailable =
|
||||||
!isUndefined(comicvine) && !isUndefined(comicvine.volumeInformation);
|
!isUndefined(comicvine) && !isUndefined((comicvine as any)?.volumeInformation);
|
||||||
|
|
||||||
// check for the availability of rawFileDetails
|
// check for the availability of rawFileDetails
|
||||||
const areRawFileDetailsAvailable =
|
const areRawFileDetailsAvailable =
|
||||||
@@ -337,7 +363,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
|||||||
query={airDCPPQuery}
|
query={airDCPPQuery}
|
||||||
comicObjectId={_id}
|
comicObjectId={_id}
|
||||||
comicObject={data.data}
|
comicObject={data.data}
|
||||||
userSettings={userSettings}
|
settings={userSettings}
|
||||||
key={4}
|
key={4}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@@ -359,8 +385,8 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
|||||||
name: "Downloads",
|
name: "Downloads",
|
||||||
icon: (
|
icon: (
|
||||||
<>
|
<>
|
||||||
{acquisition?.directconnect?.downloads?.length +
|
{(acquisition?.directconnect?.downloads?.length || 0) +
|
||||||
acquisition?.torrent.length}
|
(acquisition?.torrent?.length || 0)}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
content:
|
content:
|
||||||
@@ -397,11 +423,12 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
|||||||
imageUrl={url}
|
imageUrl={url}
|
||||||
orientation={"cover-only"}
|
orientation={"cover-only"}
|
||||||
hasDetails={false}
|
hasDetails={false}
|
||||||
|
cardContainerStyle={{ maxWidth: "290px", width: "100%" }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* raw file details */}
|
{/* raw file details */}
|
||||||
{!isUndefined(rawFileDetails) &&
|
{!isUndefined(rawFileDetails) &&
|
||||||
!isEmpty(rawFileDetails.cover) && (
|
!isEmpty((rawFileDetails as any)?.cover) && (
|
||||||
<div className="grid">
|
<div className="grid">
|
||||||
<RawFileDetails
|
<RawFileDetails
|
||||||
data={{
|
data={{
|
||||||
@@ -451,7 +478,7 @@ export const ComicDetail = (data: ComicDetailProps): ReactElement => {
|
|||||||
|
|
||||||
<TabControls
|
<TabControls
|
||||||
filteredTabs={filteredTabs}
|
filteredTabs={filteredTabs}
|
||||||
downloadCount={acquisition?.directconnect?.downloads?.length}
|
downloadCount={acquisition?.directconnect?.downloads?.length || 0}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<StyledSlidingPanel
|
<StyledSlidingPanel
|
||||||
|
|||||||
@@ -1,12 +1,21 @@
|
|||||||
import React, { ReactElement } from "react";
|
import React, { ReactElement } from "react";
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import { detectIssueTypes } from "../../shared/utils/tradepaperback.utils";
|
import { detectIssueTypes } from "../../shared/utils/tradepaperback.utils";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { isEmpty, isUndefined } from "lodash";
|
import { isEmpty, isUndefined } from "lodash";
|
||||||
import Card from "../shared/Carda";
|
import Card from "../shared/Carda";
|
||||||
import { convert } from "html-to-text";
|
import { convert } from "html-to-text";
|
||||||
|
|
||||||
export const ComicVineDetails = (props): ReactElement => {
|
interface ComicVineDetailsProps {
|
||||||
|
updatedAt?: string;
|
||||||
|
data?: {
|
||||||
|
name?: string;
|
||||||
|
number?: string;
|
||||||
|
resource_type?: string;
|
||||||
|
id?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ComicVineDetails = (props: ComicVineDetailsProps): ReactElement => {
|
||||||
const { data, updatedAt } = props;
|
const { data, updatedAt } = props;
|
||||||
return (
|
return (
|
||||||
<div className="text-slate-500 dark:text-gray-400">
|
<div className="text-slate-500 dark:text-gray-400">
|
||||||
@@ -107,13 +116,3 @@ export const ComicVineDetails = (props): ReactElement => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default ComicVineDetails;
|
export default ComicVineDetails;
|
||||||
|
|
||||||
ComicVineDetails.propTypes = {
|
|
||||||
updatedAt: PropTypes.string,
|
|
||||||
data: PropTypes.shape({
|
|
||||||
name: PropTypes.string,
|
|
||||||
number: PropTypes.string,
|
|
||||||
resource_type: PropTypes.string,
|
|
||||||
id: PropTypes.number,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,32 +1,129 @@
|
|||||||
import prettyBytes from "pretty-bytes";
|
import prettyBytes from "pretty-bytes";
|
||||||
import React, { ReactElement } from "react";
|
import React, { ReactElement, useEffect, useRef, useState } from "react";
|
||||||
|
import { useStore } from "../../store";
|
||||||
|
import type { Socket } from "socket.io-client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} DownloadProgressTickProps
|
||||||
|
* @property {string} bundleId - The bundle ID to filter ticks by (as string)
|
||||||
|
*/
|
||||||
|
interface DownloadProgressTickProps {
|
||||||
|
bundleId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shape of the download tick data received over the socket.
|
||||||
|
*
|
||||||
|
* @typedef DownloadTickData
|
||||||
|
* @property {number} id - Unique download ID
|
||||||
|
* @property {string} name - File name (e.g. "movie.mkv")
|
||||||
|
* @property {number} downloaded_bytes - Bytes downloaded so far
|
||||||
|
* @property {number} size - Total size in bytes
|
||||||
|
* @property {number} speed - Current download speed (bytes/sec)
|
||||||
|
* @property {number} seconds_left - Estimated seconds remaining
|
||||||
|
* @property {{ id: string; str: string; completed: boolean; downloaded: boolean; failed: boolean; hook_error: any }} status
|
||||||
|
* - Status object (e.g. `{ id: "queued", str: "Running (15.1%)", ... }`)
|
||||||
|
* @property {{ online: number; total: number; str: string }} sources
|
||||||
|
* - Peer count (e.g. `{ online: 1, total: 1, str: "1/1 online" }`)
|
||||||
|
* @property {string} target - Download destination (e.g. "/Downloads/movie.mkv")
|
||||||
|
*/
|
||||||
|
interface DownloadTickData {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
downloaded_bytes: number;
|
||||||
|
size: number;
|
||||||
|
speed: number;
|
||||||
|
seconds_left: number;
|
||||||
|
status: {
|
||||||
|
id: string;
|
||||||
|
str: string;
|
||||||
|
completed: boolean;
|
||||||
|
downloaded: boolean;
|
||||||
|
failed: boolean;
|
||||||
|
hook_error: any;
|
||||||
|
};
|
||||||
|
sources: {
|
||||||
|
online: number;
|
||||||
|
total: number;
|
||||||
|
str: string;
|
||||||
|
};
|
||||||
|
target: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DownloadProgressTick: React.FC<DownloadProgressTickProps> = ({
|
||||||
|
bundleId,
|
||||||
|
}): ReactElement | null => {
|
||||||
|
const socketRef = useRef<Socket>();
|
||||||
|
const [tick, setTick] = useState<DownloadTickData | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
const socket = useStore.getState().getSocket("manual");
|
||||||
|
socketRef.current = socket;
|
||||||
|
|
||||||
|
socket.emit("call", "socket.listenFileProgress", {
|
||||||
|
namespace: "/manual",
|
||||||
|
config: {
|
||||||
|
protocol: `ws`,
|
||||||
|
hostname: `192.168.1.119:5600`,
|
||||||
|
username: `admin`,
|
||||||
|
password: `password`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for each "downloadTick" event.
|
||||||
|
* Only update state if event.id matches bundleId.
|
||||||
|
*
|
||||||
|
* @param {DownloadTickData} data - Payload from the server
|
||||||
|
*/
|
||||||
|
const onDownloadTick = (data: DownloadTickData) => {
|
||||||
|
// Compare numeric data.id to string bundleId
|
||||||
|
console.log(data.id);
|
||||||
|
console.log(`bundleId is ${bundleId}`)
|
||||||
|
if (data.id === parseInt(bundleId, 10)) {
|
||||||
|
setTick(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.on("downloadTick", onDownloadTick);
|
||||||
|
return () => {
|
||||||
|
socket.off("downloadTick", onDownloadTick);
|
||||||
|
};
|
||||||
|
}, [socketRef, bundleId]);
|
||||||
|
|
||||||
|
if (!tick) {
|
||||||
|
return <>Nothing detected.</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute human-readable values and percentages
|
||||||
|
const downloaded = prettyBytes(tick.downloaded_bytes);
|
||||||
|
const total = prettyBytes(tick.size);
|
||||||
|
const percent = tick.size > 0
|
||||||
|
? Math.round((tick.downloaded_bytes / tick.size) * 100)
|
||||||
|
: 0;
|
||||||
|
const speed = prettyBytes(tick.speed) + "/s";
|
||||||
|
const minutesLeft = Math.round(tick.seconds_left / 60);
|
||||||
|
|
||||||
export const DownloadProgressTick = (props): ReactElement => {
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="mt-2 p-2 border rounded-md bg-white shadow-sm">
|
||||||
<h4 className="is-size-5">{props.data.name}</h4>
|
{/* Downloaded vs Total */}
|
||||||
<div>
|
<div className="mt-1 flex items-center space-x-2">
|
||||||
<span className="is-size-4 has-text-weight-semibold">
|
<span className="text-sm text-gray-700">{downloaded} of {total}</span>
|
||||||
{prettyBytes(props.data.downloaded_bytes)} of{" "}
|
|
||||||
{prettyBytes(props.data.size)}{" "}
|
|
||||||
</span>
|
|
||||||
<progress
|
|
||||||
className="progress is-small is-success"
|
|
||||||
value={props.data.downloaded_bytes}
|
|
||||||
max={props.data.size}
|
|
||||||
>
|
|
||||||
{(parseInt(props.data.downloaded_bytes) / parseInt(props.data.size)) *
|
|
||||||
100}
|
|
||||||
%
|
|
||||||
</progress>
|
|
||||||
</div>
|
|
||||||
<div className="is-size-6 mt-1 mb-2">
|
|
||||||
<p>{prettyBytes(props.data.speed)} per second.</p>
|
|
||||||
Time left:
|
|
||||||
{Math.round(parseInt(props.data.seconds_left) / 60)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>{props.data.target}</div>
|
{/* Progress bar */}
|
||||||
|
<div className="relative mt-2 h-2 bg-gray-200 rounded overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="absolute inset-y-0 left-0 bg-green-500"
|
||||||
|
style={{ width: `${percent}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 text-xs text-gray-600">{percent}% complete</div>
|
||||||
|
|
||||||
|
{/* Speed and Time Left */}
|
||||||
|
<div className="mt-2 flex space-x-4 text-xs text-gray-600">
|
||||||
|
<span>Speed: {speed}</span>
|
||||||
|
<span>Time left: {minutesLeft} min</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useEffect, useContext, ReactElement, useState } from "react";
|
import React, { useEffect, ReactElement, useState, useMemo } from "react";
|
||||||
import { RootState } from "threetwo-ui-typings";
|
import { isEmpty, isNil, isUndefined, map } from "lodash";
|
||||||
import { isEmpty, map } from "lodash";
|
|
||||||
import { AirDCPPBundles } from "./AirDCPPBundles";
|
import { AirDCPPBundles } from "./AirDCPPBundles";
|
||||||
import { TorrentDownloads } from "./TorrentDownloads";
|
import { TorrentDownloads } from "./TorrentDownloads";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
@@ -14,132 +13,142 @@ import { useStore } from "../../store";
|
|||||||
import { useShallow } from "zustand/react/shallow";
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
|
|
||||||
interface IDownloadsPanelProps {
|
export interface TorrentDetails {
|
||||||
key: number;
|
infoHash: string;
|
||||||
|
progress: number;
|
||||||
|
downloadSpeed?: number;
|
||||||
|
uploadSpeed?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DownloadsPanel = (
|
/**
|
||||||
props: IDownloadsPanelProps,
|
* DownloadsPanel displays two tabs of download information for a specific comic:
|
||||||
): ReactElement | null => {
|
* - DC++ (AirDCPP) bundles
|
||||||
|
* - Torrent downloads
|
||||||
|
* It also listens for real-time torrent updates via a WebSocket.
|
||||||
|
*
|
||||||
|
* @component
|
||||||
|
* @returns {ReactElement | null} The rendered DownloadsPanel or null if no socket is available.
|
||||||
|
*/
|
||||||
|
export const DownloadsPanel = (): ReactElement | null => {
|
||||||
const { comicObjectId } = useParams<{ comicObjectId: string }>();
|
const { comicObjectId } = useParams<{ comicObjectId: string }>();
|
||||||
const [bundles, setBundles] = useState([]);
|
|
||||||
const [infoHashes, setInfoHashes] = useState<string[]>([]);
|
const [infoHashes, setInfoHashes] = useState<string[]>([]);
|
||||||
const [torrentDetails, setTorrentDetails] = useState([]);
|
const [torrentDetails, setTorrentDetails] = useState<TorrentDetails[]>([]);
|
||||||
const [activeTab, setActiveTab] = useState("torrents");
|
const [activeTab, setActiveTab] = useState<"directconnect" | "torrents">(
|
||||||
const { airDCPPSocketInstance, socketIOInstance } = useStore(
|
"directconnect",
|
||||||
useShallow((state: any) => ({
|
|
||||||
airDCPPSocketInstance: state.airDCPPSocketInstance,
|
|
||||||
socketIOInstance: state.socketIOInstance,
|
|
||||||
})),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// React to torrent progress data sent over websockets
|
const { socketIOInstance } = useStore(
|
||||||
socketIOInstance.on("AS_TORRENT_DATA", (data) => {
|
useShallow((state: any) => ({ socketIOInstance: state.socketIOInstance })),
|
||||||
const torrents = data.torrents
|
);
|
||||||
.flatMap(({ _id, details }) => {
|
|
||||||
if (_id === comicObjectId) {
|
/**
|
||||||
return details;
|
* Registers socket listeners on mount and cleans up on unmount.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (!socketIOInstance) return;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for incoming torrent data events.
|
||||||
|
* Merges new entries or updates existing ones by infoHash.
|
||||||
|
*
|
||||||
|
* @param {TorrentDetails} data - Payload from the socket event.
|
||||||
|
*/
|
||||||
|
const handleTorrentData = (data: TorrentDetails) => {
|
||||||
|
setTorrentDetails((prev) => {
|
||||||
|
const idx = prev.findIndex((t) => t.infoHash === data.infoHash);
|
||||||
|
if (idx === -1) {
|
||||||
|
return [...prev, data];
|
||||||
}
|
}
|
||||||
})
|
const next = [...prev];
|
||||||
.filter((item) => item !== undefined);
|
next[idx] = { ...next[idx], ...data };
|
||||||
setTorrentDetails(torrents);
|
return next;
|
||||||
});
|
});
|
||||||
// Fetch the downloaded files and currently-downloading file(s) from AirDC++
|
};
|
||||||
const { data: comicObject, isSuccess } = useQuery({
|
|
||||||
queryKey: ["bundles"],
|
socketIOInstance.on("AS_TORRENT_DATA", handleTorrentData);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socketIOInstance.off("AS_TORRENT_DATA", handleTorrentData);
|
||||||
|
};
|
||||||
|
}, [socketIOInstance]);
|
||||||
|
|
||||||
|
// ————— DC++ Bundles (via REST) —————
|
||||||
|
const { data: bundles } = useQuery({
|
||||||
|
queryKey: ["bundles", comicObjectId],
|
||||||
queryFn: async () =>
|
queryFn: async () =>
|
||||||
await axios({
|
await axios({
|
||||||
url: `${LIBRARY_SERVICE_BASE_URI}/getComicBookById`,
|
url: `${LIBRARY_SERVICE_BASE_URI}/getBundles`,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json; charset=utf-8",
|
|
||||||
},
|
|
||||||
data: {
|
data: {
|
||||||
id: `${comicObjectId}`,
|
comicObjectId,
|
||||||
},
|
config: {
|
||||||
}),
|
protocol: `ws`,
|
||||||
});
|
hostname: `192.168.1.119:5600`,
|
||||||
const getBundles = async (comicObject) => {
|
username: `admin`,
|
||||||
if (comicObject?.data.acquisition.directconnect) {
|
password: `password`,
|
||||||
const filteredBundles =
|
|
||||||
comicObject.data.acquisition.directconnect.downloads.map(
|
|
||||||
async ({ bundleId }) => {
|
|
||||||
return await airDCPPSocketInstance.get(`queue/bundles/${bundleId}`);
|
|
||||||
},
|
},
|
||||||
);
|
|
||||||
return await Promise.all(filteredBundles);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Call the scheduled job for fetching torrent data
|
|
||||||
// triggered by the active tab been set to "torrents"
|
|
||||||
const { data: torrentData } = useQuery({
|
|
||||||
queryFn: () =>
|
|
||||||
axios({
|
|
||||||
url: `${TORRENT_JOB_SERVICE_BASE_URI}/getTorrentData`,
|
|
||||||
method: "GET",
|
|
||||||
params: {
|
|
||||||
trigger: activeTab,
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
queryKey: [activeTab],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ————— Torrent Jobs (via REST) —————
|
||||||
|
const { data: rawJobs = [] } = useQuery<any[]>({
|
||||||
|
queryKey: ["torrents", comicObjectId],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await axios.get(
|
||||||
|
`${TORRENT_JOB_SERVICE_BASE_URI}/getTorrentData`,
|
||||||
|
{ params: { trigger: activeTab } },
|
||||||
|
);
|
||||||
|
return Array.isArray(data) ? data : [];
|
||||||
|
},
|
||||||
|
initialData: [],
|
||||||
|
enabled: activeTab === "torrents",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only when rawJobs changes *and* activeTab === "torrents" should we update infoHashes:
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getBundles(comicObject).then((result) => {
|
if (activeTab !== "torrents") return;
|
||||||
setBundles(result);
|
setInfoHashes(rawJobs.map((j: any) => j.infoHash));
|
||||||
});
|
}, [activeTab]);
|
||||||
}, [comicObject]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="columns is-multiline">
|
<>
|
||||||
<div>
|
<div className="mt-5 mb-3">
|
||||||
<div className="sm:hidden">
|
<nav className="flex space-x-2">
|
||||||
<label htmlFor="Download Type" className="sr-only">
|
<button
|
||||||
Download Type
|
onClick={() => setActiveTab("directconnect")}
|
||||||
</label>
|
className={`px-4 py-1 rounded-full text-sm font-medium transition-colors ${
|
||||||
|
activeTab === "directconnect"
|
||||||
|
? "bg-green-500 text-white"
|
||||||
|
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
DC++
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab("torrents")}
|
||||||
|
className={`px-4 py-1 rounded-full text-sm font-medium transition-colors ${
|
||||||
|
activeTab === "torrents"
|
||||||
|
? "bg-blue-500 text-white"
|
||||||
|
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Torrents
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<select id="Tab" className="w-full rounded-md border-gray-200">
|
<div className="mt-4">
|
||||||
<option>DC++ Downloads</option>
|
{activeTab === "torrents" ? (
|
||||||
<option>Torrents</option>
|
<TorrentDownloads data={torrentDetails} />
|
||||||
</select>
|
) : !isNil(bundles?.data) && bundles.data.length > 0 ? (
|
||||||
</div>
|
<AirDCPPBundles data={bundles.data} />
|
||||||
|
) : (
|
||||||
<div className="hidden sm:block">
|
<p>No DC++ bundles found.</p>
|
||||||
<nav className="flex gap-6" aria-label="Tabs">
|
)}
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className={`shrink-0 rounded-lg p-2 text-sm font-medium hover:bg-gray-50 hover:text-gray-700 ${
|
|
||||||
activeTab === "directconnect"
|
|
||||||
? "bg-slate-200 dark:text-slate-200 dark:bg-slate-400 text-slate-800"
|
|
||||||
: "dark:text-slate-400 text-slate-800"
|
|
||||||
}`}
|
|
||||||
aria-current="page"
|
|
||||||
onClick={() => setActiveTab("directconnect")}
|
|
||||||
>
|
|
||||||
DC++ Downloads
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className={`shrink-0 rounded-lg p-2 text-sm font-medium hover:bg-gray-50 hover:text-gray-700 ${
|
|
||||||
activeTab === "torrents"
|
|
||||||
? "bg-slate-200 text-slate-800"
|
|
||||||
: "dark:text-slate-400 text-slate-800"
|
|
||||||
}`}
|
|
||||||
onClick={() => setActiveTab("torrents")}
|
|
||||||
>
|
|
||||||
Torrents
|
|
||||||
</a>
|
|
||||||
</nav>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
{activeTab === "torrents" && <TorrentDownloads data={torrentDetails} />}
|
|
||||||
{!isEmpty(airDCPPSocketInstance) &&
|
|
||||||
!isEmpty(bundles) &&
|
|
||||||
activeTab === "directconnect" && <AirDCPPBundles data={bundles} />}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DownloadsPanel;
|
export default DownloadsPanel;
|
||||||
|
|||||||
@@ -1,127 +1,157 @@
|
|||||||
import React, { ReactElement } from "react";
|
import React, { ReactElement } from "react";
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import prettyBytes from "pretty-bytes";
|
import prettyBytes from "pretty-bytes";
|
||||||
import { isEmpty } from "lodash";
|
import { isEmpty } from "lodash";
|
||||||
import { format, parseISO } from "date-fns";
|
import { format, parseISO } from "date-fns";
|
||||||
|
|
||||||
export const RawFileDetails = (props): ReactElement => {
|
interface RawFileDetailsProps {
|
||||||
|
data?: {
|
||||||
|
rawFileDetails?: {
|
||||||
|
containedIn?: string;
|
||||||
|
name?: string;
|
||||||
|
fileSize?: number;
|
||||||
|
path?: string;
|
||||||
|
extension?: string;
|
||||||
|
mimeType?: string;
|
||||||
|
cover?: {
|
||||||
|
filePath?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
inferredMetadata?: {
|
||||||
|
issue?: {
|
||||||
|
year?: string;
|
||||||
|
name?: string;
|
||||||
|
number?: number;
|
||||||
|
subtitle?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
};
|
||||||
|
children?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RawFileDetails = (props: RawFileDetailsProps): ReactElement | null => {
|
||||||
const { rawFileDetails, inferredMetadata, created_at, updated_at } =
|
const { rawFileDetails, inferredMetadata, created_at, updated_at } =
|
||||||
props.data;
|
props.data || {};
|
||||||
|
if (!rawFileDetails) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="max-w-2xl ml-5">
|
<div className="max-w-2xl ml-5">
|
||||||
<div className="px-4 sm:px-6">
|
{/* Title */}
|
||||||
|
<div className="px-4 sm:px-6 mb-6">
|
||||||
<p className="text-gray-500 dark:text-gray-400">
|
<p className="text-gray-500 dark:text-gray-400">
|
||||||
<span className="text-xl">{rawFileDetails.name}</span>
|
<span className="text-xl font-semibold">{rawFileDetails?.name}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-4 py-5 sm:px-6">
|
|
||||||
<dl className="grid grid-cols-1 gap-x-4 gap-y-4 sm:grid-cols-2">
|
{/* File Binary Details Section */}
|
||||||
<div className="sm:col-span-1">
|
<div className="mb-8 px-4 pb-8 border-b border-gray-200 dark:border-gray-700">
|
||||||
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">
|
<div className="mb-4">
|
||||||
Raw File Details
|
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-300 uppercase tracking-wide flex items-center gap-1">
|
||||||
</dt>
|
<i className="icon-[solar--document-bold-duotone] w-5 h-5"></i>
|
||||||
<dd className="mt-1 text-sm text-gray-900 dark:text-gray-400">
|
File Binary Details
|
||||||
{rawFileDetails.containedIn +
|
</h3>
|
||||||
"/" +
|
</div>
|
||||||
rawFileDetails.name +
|
<div className="pl-6">
|
||||||
rawFileDetails.extension}
|
<dl className="space-y-4">
|
||||||
</dd>
|
<div>
|
||||||
</div>
|
<dt className="text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wide mb-1">
|
||||||
<div className="sm:col-span-1">
|
File Path
|
||||||
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">
|
</dt>
|
||||||
Inferred Issue Metadata
|
<dd className="text-sm text-gray-900 dark:text-gray-300 font-mono break-all">
|
||||||
</dt>
|
{rawFileDetails?.containedIn}/{rawFileDetails?.name}{rawFileDetails?.extension}
|
||||||
<dd className="mt-1 text-sm text-gray-900 dark:text-gray-400">
|
</dd>
|
||||||
Series Name: {inferredMetadata.issue.name}
|
</div>
|
||||||
{!isEmpty(inferredMetadata.issue.number) ? (
|
|
||||||
<span className="tag is-primary is-light">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
{inferredMetadata.issue.number}
|
<div>
|
||||||
</span>
|
<dt className="text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wide mb-1">
|
||||||
) : null}
|
MIME Type
|
||||||
</dd>
|
</dt>
|
||||||
</div>
|
<dd className="text-sm text-gray-700 dark:text-gray-300 flex items-center gap-1">
|
||||||
<div className="sm:col-span-1">
|
|
||||||
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">
|
|
||||||
MIMEType
|
|
||||||
</dt>
|
|
||||||
<dd className="mt-1 text-sm text-gray-500 dark:text-slate-900">
|
|
||||||
{/* File extension */}
|
|
||||||
<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="pt-1">
|
|
||||||
<i className="icon-[solar--zip-file-bold-duotone] w-5 h-5"></i>
|
<i className="icon-[solar--zip-file-bold-duotone] w-5 h-5"></i>
|
||||||
</span>
|
{rawFileDetails?.mimeType}
|
||||||
|
</dd>
|
||||||
<span className="text-md text-slate-500 dark:text-slate-900">
|
</div>
|
||||||
{rawFileDetails.mimeType}
|
|
||||||
</span>
|
<div>
|
||||||
</span>
|
<dt className="text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wide mb-1">
|
||||||
</dd>
|
File Size
|
||||||
</div>
|
</dt>
|
||||||
<div className="sm:col-span-1">
|
<dd className="text-sm text-gray-700 dark:text-gray-300 flex items-center gap-1">
|
||||||
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">
|
|
||||||
File Size
|
|
||||||
</dt>
|
|
||||||
<dd className="mt-1 text-sm text-gray-500 dark:text-slate-900">
|
|
||||||
{/* size */}
|
|
||||||
<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--mirror-right-bold-duotone] w-5 h-5"></i>
|
<i className="icon-[solar--mirror-right-bold-duotone] w-5 h-5"></i>
|
||||||
</span>
|
{rawFileDetails?.fileSize ? prettyBytes(rawFileDetails.fileSize) : 'N/A'}
|
||||||
|
</dd>
|
||||||
<span className="text-md text-slate-500 dark:text-slate-900">
|
</div>
|
||||||
{prettyBytes(rawFileDetails.fileSize)}
|
</div>
|
||||||
</span>
|
</dl>
|
||||||
</span>
|
</div>
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
<div className="sm:col-span-2">
|
|
||||||
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">
|
|
||||||
Import Details
|
|
||||||
</dt>
|
|
||||||
<dd className="mt-1 text-sm text-gray-900 dark:text-gray-400">
|
|
||||||
{format(parseISO(created_at), "dd MMMM, yyyy")},{" "}
|
|
||||||
{format(parseISO(created_at), "h aaaa")}
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
<div className="sm:col-span-2">
|
|
||||||
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">
|
|
||||||
Actions
|
|
||||||
</dt>
|
|
||||||
<dd className="mt-1 text-sm text-gray-900">{props.children}</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Import Details Section */}
|
||||||
|
<div className="mb-8 px-4">
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-300 uppercase tracking-wide flex items-center gap-1">
|
||||||
|
<i className="icon-[solar--import-bold-duotone] w-5 h-5"></i>
|
||||||
|
Import Details
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="pl-6">
|
||||||
|
<dl className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<dt className="text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wide mb-1">
|
||||||
|
Imported On
|
||||||
|
</dt>
|
||||||
|
<dd className="text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
{created_at ? format(parseISO(created_at), "dd MMMM, yyyy 'at' h:mm aaaa") : 'N/A'}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<dt className="text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wide mb-1">
|
||||||
|
Last Updated
|
||||||
|
</dt>
|
||||||
|
<dd className="text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
{updated_at ? format(parseISO(updated_at), "dd MMMM, yyyy 'at' h:mm aaaa") : 'N/A'}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{inferredMetadata?.issue && (
|
||||||
|
<div>
|
||||||
|
<dt className="text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wide mb-1">
|
||||||
|
Inferred Metadata
|
||||||
|
</dt>
|
||||||
|
<dd className="text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
<span className="font-medium">{inferredMetadata.issue.name}</span>
|
||||||
|
{!isEmpty(inferredMetadata.issue.number) && (
|
||||||
|
<span className="ml-2 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
#{inferredMetadata.issue.number}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions Section */}
|
||||||
|
{props.children && (
|
||||||
|
<div className="px-4">
|
||||||
|
<div className="mb-3">
|
||||||
|
<h4 className="text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wide">
|
||||||
|
Actions
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div>{props.children}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RawFileDetails;
|
export default RawFileDetails;
|
||||||
|
|
||||||
RawFileDetails.propTypes = {
|
|
||||||
data: PropTypes.shape({
|
|
||||||
rawFileDetails: PropTypes.shape({
|
|
||||||
containedIn: PropTypes.string,
|
|
||||||
name: PropTypes.string,
|
|
||||||
fileSize: PropTypes.number,
|
|
||||||
path: PropTypes.string,
|
|
||||||
extension: PropTypes.string,
|
|
||||||
mimeType: PropTypes.string,
|
|
||||||
cover: PropTypes.shape({
|
|
||||||
filePath: PropTypes.string,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
inferredMetadata: PropTypes.shape({
|
|
||||||
issue: PropTypes.shape({
|
|
||||||
year: PropTypes.string,
|
|
||||||
name: PropTypes.string,
|
|
||||||
number: PropTypes.number,
|
|
||||||
subtitle: PropTypes.string,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
created_at: PropTypes.string,
|
|
||||||
updated_at: PropTypes.string,
|
|
||||||
}),
|
|
||||||
children: PropTypes.any,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -14,12 +14,16 @@ import { useStore } from "../../../store";
|
|||||||
import { useShallow } from "zustand/react/shallow";
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import { escapePoundSymbol } from "../../../shared/utils/formatting.utils";
|
import { escapePoundSymbol } from "../../../shared/utils/formatting.utils";
|
||||||
|
|
||||||
export const ArchiveOperations = (props): ReactElement => {
|
interface ArchiveOperationsProps {
|
||||||
|
data: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ArchiveOperations = (props: ArchiveOperationsProps): ReactElement => {
|
||||||
const { data } = props;
|
const { data } = props;
|
||||||
|
|
||||||
const { socketIOInstance } = useStore(
|
const { getSocket } = useStore(
|
||||||
useShallow((state) => ({
|
useShallow((state) => ({
|
||||||
socketIOInstance: state.socketIOInstance,
|
getSocket: state.getSocket,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -27,21 +31,32 @@ export const ArchiveOperations = (props): ReactElement => {
|
|||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const [slidingPanelContentId, setSlidingPanelContentId] = useState("");
|
const [slidingPanelContentId, setSlidingPanelContentId] = useState("");
|
||||||
// current image
|
// current image
|
||||||
const [currentImage, setCurrentImage] = useState([]);
|
const [currentImage, setCurrentImage] = useState<string>("");
|
||||||
const [uncompressedArchive, setUncompressedArchive] = useState([]);
|
const [uncompressedArchive, setUncompressedArchive] = useState<string[]>([]);
|
||||||
const [imageAnalysisResult, setImageAnalysisResult] = useState({});
|
const [imageAnalysisResult, setImageAnalysisResult] = useState<any>({});
|
||||||
const [shouldRefetchComicBookData, setShouldRefetchComicBookData] =
|
const [shouldRefetchComicBookData, setShouldRefetchComicBookData] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const constructImagePaths = (data): Array<string> => {
|
const constructImagePaths = (data: string[]): Array<string> => {
|
||||||
return data?.map((path: string) =>
|
return data?.map((path: string) =>
|
||||||
escapePoundSymbol(encodeURI(`${LIBRARY_SERVICE_HOST}/${path}`)),
|
escapePoundSymbol(encodeURI(`${LIBRARY_SERVICE_HOST}/${path}`)),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Listen to the uncompression complete event and orchestrate the final payload
|
// Listen to the uncompression complete event and orchestrate the final payload
|
||||||
socketIOInstance.on("LS_UNCOMPRESSION_JOB_COMPLETE", (data) => {
|
useEffect(() => {
|
||||||
setUncompressedArchive(constructImagePaths(data?.uncompressedArchive));
|
const socket = getSocket("/");
|
||||||
});
|
|
||||||
|
const handleUncompressionComplete = (data: any) => {
|
||||||
|
setUncompressedArchive(constructImagePaths(data?.uncompressedArchive));
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.on("LS_UNCOMPRESSION_JOB_COMPLETE", handleUncompressionComplete);
|
||||||
|
|
||||||
|
// Cleanup listener on unmount
|
||||||
|
return () => {
|
||||||
|
socket.off("LS_UNCOMPRESSION_JOB_COMPLETE", handleUncompressionComplete);
|
||||||
|
};
|
||||||
|
}, [getSocket]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isMounted = true;
|
let isMounted = true;
|
||||||
@@ -58,7 +73,7 @@ export const ArchiveOperations = (props): ReactElement => {
|
|||||||
},
|
},
|
||||||
transformResponse: async (responseData) => {
|
transformResponse: async (responseData) => {
|
||||||
const parsedData = JSON.parse(responseData);
|
const parsedData = JSON.parse(responseData);
|
||||||
const paths = parsedData.map((pathObject) => {
|
const paths = parsedData.map((pathObject: any) => {
|
||||||
return `${pathObject.containedIn}/${pathObject.name}${pathObject.extension}`;
|
return `${pathObject.containedIn}/${pathObject.name}${pathObject.extension}`;
|
||||||
});
|
});
|
||||||
const uncompressedArchive = constructImagePaths(paths);
|
const uncompressedArchive = constructImagePaths(paths);
|
||||||
@@ -131,7 +146,7 @@ export const ArchiveOperations = (props): ReactElement => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sliding panel init
|
// sliding panel init
|
||||||
const contentForSlidingPanel = {
|
const contentForSlidingPanel: Record<string, { content: () => JSX.Element }> = {
|
||||||
imageAnalysis: {
|
imageAnalysis: {
|
||||||
content: () => {
|
content: () => {
|
||||||
return (
|
return (
|
||||||
@@ -143,7 +158,7 @@ export const ArchiveOperations = (props): ReactElement => {
|
|||||||
</pre>
|
</pre>
|
||||||
) : null}
|
) : null}
|
||||||
<pre className="font-hasklig mt-3 text-sm">
|
<pre className="font-hasklig mt-3 text-sm">
|
||||||
{JSON.stringify(imageAnalysisResult.analyzedData, null, 2)}
|
{JSON.stringify(imageAnalysisResult?.analyzedData, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -152,7 +167,7 @@ export const ArchiveOperations = (props): ReactElement => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// sliding panel handlers
|
// sliding panel handlers
|
||||||
const openImageAnalysisPanel = useCallback((imageFilePath) => {
|
const openImageAnalysisPanel = useCallback((imageFilePath: string) => {
|
||||||
setSlidingPanelContentId("imageAnalysis");
|
setSlidingPanelContentId("imageAnalysis");
|
||||||
analyzeImage(imageFilePath);
|
analyzeImage(imageFilePath);
|
||||||
setCurrentImage(imageFilePath);
|
setCurrentImage(imageFilePath);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const TorrentSearchPanel = (props) => {
|
|||||||
data: {
|
data: {
|
||||||
prowlarrQuery: {
|
prowlarrQuery: {
|
||||||
port: "9696",
|
port: "9696",
|
||||||
apiKey: "c4f42e265fb044dc81f7e88bd41c3367",
|
apiKey: "38c2656e8f5d4790962037b8c4416a8f",
|
||||||
offset: 0,
|
offset: 0,
|
||||||
categories: [7030],
|
categories: [7030],
|
||||||
query: searchTerm.issueName,
|
query: searchTerm.issueName,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { ReactElement, useState } from "react";
|
import React, { ReactElement, useState, useEffect } from "react";
|
||||||
import { map } from "lodash";
|
import { map } from "lodash";
|
||||||
import Card from "../shared/Carda";
|
import Card from "../shared/Carda";
|
||||||
import Header from "../shared/Header";
|
import Header from "../shared/Header";
|
||||||
@@ -31,27 +31,38 @@ export const PullList = (): ReactElement => {
|
|||||||
// datepicker
|
// datepicker
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
const [inputValue, setInputValue] = useState<string>(
|
const [inputValue, setInputValue] = useState<string>(
|
||||||
format(date, "M-dd-yyyy"),
|
format(date, "yyyy/M/dd"),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Responsive slides per view
|
||||||
|
const [slidesPerView, setSlidesPerView] = useState(1);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// keen slider
|
// keen slider
|
||||||
const [sliderRef, instanceRef] = useKeenSlider(
|
const [sliderRef, instanceRef] = useKeenSlider({
|
||||||
{
|
loop: true,
|
||||||
loop: true,
|
mode: "free-snap",
|
||||||
slides: {
|
slides: {
|
||||||
origin: "auto",
|
perView: slidesPerView,
|
||||||
number: 15,
|
spacing: 15,
|
||||||
perView: 5,
|
|
||||||
spacing: 15,
|
|
||||||
},
|
|
||||||
slideChanged() {
|
|
||||||
console.log("slide changed");
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
[
|
slideChanged() {
|
||||||
// add plugins here
|
console.log("slide changed");
|
||||||
],
|
},
|
||||||
);
|
});
|
||||||
|
|
||||||
|
// Update slider when slidesPerView changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (instanceRef.current) {
|
||||||
|
instanceRef.current.update({
|
||||||
|
slides: {
|
||||||
|
perView: slidesPerView,
|
||||||
|
spacing: 15,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [slidesPerView, instanceRef]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: pullList,
|
data: pullList,
|
||||||
@@ -80,79 +91,81 @@ export const PullList = (): ReactElement => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="content">
|
<div className="content">
|
||||||
<Header
|
<div className="mx-auto">
|
||||||
headerContent="Discover"
|
<Header
|
||||||
subHeaderContent={
|
headerContent="Discover"
|
||||||
<span className="text-md">
|
subHeaderContent={
|
||||||
Pull List aggregated for the week from{" "}
|
<span className="text-md">
|
||||||
<span className="underline">
|
Pull List aggregated for the week from{" "}
|
||||||
<a href="https://leagueofcomicgeeks.com/comics/new-comics">
|
<span className="underline">
|
||||||
League Of Comic Geeks
|
<a href="https://leagueofcomicgeeks.com">
|
||||||
</a>
|
League Of Comic Geeks
|
||||||
<i className="icon-[solar--arrow-right-up-outline] w-4 h-4" />
|
</a>
|
||||||
|
<i className="icon-[solar--arrow-right-up-outline] w-4 h-4" />
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
}
|
||||||
}
|
iconClassNames="fa-solid fa-binoculars mr-2"
|
||||||
iconClassNames="fa-solid fa-binoculars mr-2"
|
link="/pull-list/all/"
|
||||||
link="/pull-list/all/"
|
/>
|
||||||
/>
|
<div className="flex flex-row gap-5 mb-3">
|
||||||
<div className="flex flex-row gap-5 mb-3">
|
{/* select week */}
|
||||||
{/* select week */}
|
<div className="flex flex-row gap-4 my-3">
|
||||||
<div className="flex flex-row gap-4 my-3">
|
<Form
|
||||||
<Form
|
onSubmit={() => {}}
|
||||||
onSubmit={() => {}}
|
render={({ handleSubmit }) => (
|
||||||
render={({ handleSubmit }) => (
|
<form>
|
||||||
<form>
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex flex-col gap-2">
|
{/* week selection for pull list */}
|
||||||
{/* week selection for pull list */}
|
<DatePickerDialog
|
||||||
<DatePickerDialog
|
inputValue={inputValue}
|
||||||
inputValue={inputValue}
|
setter={setInputValue}
|
||||||
setter={setInputValue}
|
/>
|
||||||
/>
|
{inputValue && (
|
||||||
{inputValue && (
|
<div className="text-sm">
|
||||||
<div className="text-sm">
|
Showing pull list for{" "}
|
||||||
Showing pull list for{" "}
|
<span className="inline-flex mb-2 items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-1 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||||
<span className="inline-flex mb-2 items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-1 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
{inputValue}
|
||||||
{inputValue}
|
</span>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</form>
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isSuccess && !isLoading && (
|
{isSuccess && !isLoading && (
|
||||||
<div ref={sliderRef} className="keen-slider flex flex-row">
|
<div ref={sliderRef} className="keen-slider">
|
||||||
{map(pullList?.data.result, (issue, idx) => {
|
{map(pullList?.data.result, (issue, idx) => {
|
||||||
return (
|
return (
|
||||||
<div key={idx} className="keen-slider__slide">
|
<div key={idx} className="keen-slider__slide">
|
||||||
<Card
|
<Card
|
||||||
orientation={"vertical-2"}
|
orientation={"vertical-2"}
|
||||||
imageUrl={issue.cover}
|
imageUrl={issue.coverImageUrl}
|
||||||
hasDetails
|
hasDetails
|
||||||
title={ellipsize(issue.name, 25)}
|
title={ellipsize(issue.issueName, 25)}
|
||||||
>
|
>
|
||||||
<div className="px-1">
|
<div className="px-1">
|
||||||
<span className="inline-flex mb-2 items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-1 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
<span className="inline-flex mb-2 items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-1 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||||
{issue.publisher}
|
{issue.publicationDate}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex flex-row justify-end">
|
<div className="flex flex-row justify-end">
|
||||||
<button
|
<button
|
||||||
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"
|
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"
|
||||||
onClick={() => addToLibrary("locg", issue)}
|
onClick={() => addToLibrary("locg", issue)}
|
||||||
>
|
>
|
||||||
<i className="icon-[solar--add-square-bold-duotone] w-5 h-5 mr-2"></i>{" "}
|
<i className="icon-[solar--add-square-bold-duotone] w-5 h-5 mr-2"></i>{" "}
|
||||||
Want
|
Want
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
);
|
</div>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -21,12 +21,13 @@ export const RecentlyImported = (
|
|||||||
console.log(comics);
|
console.log(comics);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Header
|
<div className="mx-auto" style={{ maxWidth: '1400px' }}>
|
||||||
headerContent="Recently Imported"
|
<Header
|
||||||
subHeaderContent="Recent Library activity such as imports, tagging, etc."
|
headerContent="Recently Imported"
|
||||||
iconClassNames="fa-solid fa-binoculars mr-2"
|
subHeaderContent="Recent Library activity such as imports, tagging, etc."
|
||||||
/>
|
iconClassNames="fa-solid fa-binoculars mr-2"
|
||||||
<div className="grid grid-cols-5 gap-6 mt-3">
|
/>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 xl:grid-cols-5 gap-6 mt-3">
|
||||||
{comics?.comics.map(
|
{comics?.comics.map(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
@@ -125,6 +126,7 @@ export const RecentlyImported = (
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { isEmpty, isNil, isUndefined, map } from "lodash";
|
|||||||
import { detectIssueTypes } from "../../shared/utils/tradepaperback.utils";
|
import { detectIssueTypes } from "../../shared/utils/tradepaperback.utils";
|
||||||
import { determineCoverFile } from "../../shared/utils/metadata.utils";
|
import { determineCoverFile } from "../../shared/utils/metadata.utils";
|
||||||
import Header from "../shared/Header";
|
import Header from "../shared/Header";
|
||||||
|
import useEmblaCarousel from "embla-carousel-react";
|
||||||
|
|
||||||
type WantedComicsListProps = {
|
type WantedComicsListProps = {
|
||||||
comics: any;
|
comics: any;
|
||||||
@@ -16,107 +17,126 @@ export const WantedComicsList = ({
|
|||||||
}: WantedComicsListProps): ReactElement => {
|
}: WantedComicsListProps): ReactElement => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// embla carousel
|
||||||
|
const [emblaRef, emblaApi] = useEmblaCarousel({
|
||||||
|
loop: false,
|
||||||
|
align: "start",
|
||||||
|
containScroll: "trimSnaps",
|
||||||
|
slidesToScroll: 1,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div>
|
||||||
<Header
|
<Header
|
||||||
headerContent="Wanted Comics"
|
headerContent="Wanted Comics"
|
||||||
subHeaderContent="Comics marked as wanted from various sources"
|
subHeaderContent="Comics marked as wanted from various sources"
|
||||||
iconClassNames="fa-solid fa-binoculars mr-2"
|
iconClassNames="fa-solid fa-binoculars mr-2"
|
||||||
link={"/wanted"}
|
link={"/wanted"}
|
||||||
/>
|
/>
|
||||||
<div className="grid grid-cols-5 gap-6 mt-3">
|
<div className="overflow-hidden -mr-4 sm:-mr-8 lg:-mr-16 xl:-mr-20 2xl:-mr-24 mt-3">
|
||||||
{map(
|
<div className="overflow-hidden" ref={emblaRef}>
|
||||||
comics,
|
<div className="flex">
|
||||||
({
|
{map(
|
||||||
_id,
|
comics,
|
||||||
rawFileDetails,
|
(
|
||||||
sourcedMetadata: { comicvine, comicInfo, locg },
|
{
|
||||||
wanted,
|
_id,
|
||||||
}) => {
|
rawFileDetails,
|
||||||
const isComicBookMetadataAvailable = !isUndefined(comicvine);
|
sourcedMetadata: { comicvine, comicInfo, locg },
|
||||||
const consolidatedComicMetadata = {
|
wanted,
|
||||||
rawFileDetails,
|
},
|
||||||
comicvine,
|
idx,
|
||||||
comicInfo,
|
) => {
|
||||||
locg,
|
const isComicBookMetadataAvailable = !isUndefined(comicvine);
|
||||||
};
|
const consolidatedComicMetadata = {
|
||||||
|
rawFileDetails,
|
||||||
|
comicvine,
|
||||||
|
comicInfo,
|
||||||
|
locg,
|
||||||
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
issueName,
|
issueName,
|
||||||
url,
|
url,
|
||||||
publisher = null,
|
publisher = null,
|
||||||
} = determineCoverFile(consolidatedComicMetadata);
|
} = determineCoverFile(consolidatedComicMetadata);
|
||||||
const titleElement = (
|
const titleElement = (
|
||||||
<Link to={"/comic/details/" + _id}>
|
<Link to={"/comic/details/" + _id}>
|
||||||
{ellipsize(issueName, 20)}
|
{ellipsize(issueName, 20)}
|
||||||
<p>{publisher}</p>
|
<p>{publisher}</p>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Card
|
<div
|
||||||
key={_id}
|
key={idx}
|
||||||
orientation={"vertical-2"}
|
className="flex-[0_0_200px] min-w-0 sm:flex-[0_0_220px] md:flex-[0_0_240px] lg:flex-[0_0_260px] xl:flex-[0_0_280px] pr-[15px]"
|
||||||
imageUrl={url}
|
>
|
||||||
hasDetails
|
<Card
|
||||||
title={issueName ? titleElement : <span>No Name</span>}
|
orientation={"vertical-2"}
|
||||||
>
|
imageUrl={url}
|
||||||
<div className="pb-1">
|
hasDetails
|
||||||
<div className="flex flex-row gap-2">
|
title={issueName ? titleElement : <span>No Name</span>}
|
||||||
{/* Issue type */}
|
>
|
||||||
{isComicBookMetadataAvailable &&
|
<div className="pb-1">
|
||||||
!isNil(detectIssueTypes(comicvine.description)) ? (
|
<div className="flex flex-row gap-2">
|
||||||
<div className="my-2">
|
{/* Issue type */}
|
||||||
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-0.5 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
{isComicBookMetadataAvailable &&
|
||||||
<span className="pr-1 pt-1">
|
!isNil(detectIssueTypes(comicvine.description)) ? (
|
||||||
<i className="icon-[solar--book-2-line-duotone] w-5 h-5"></i>
|
<div className="my-2">
|
||||||
</span>
|
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-0.5 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">
|
<span className="text-md text-slate-500 dark:text-slate-900">
|
||||||
{
|
{
|
||||||
detectIssueTypes(comicvine.description)
|
detectIssueTypes(comicvine.description)
|
||||||
.displayName
|
.displayName
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{/* issues marked as wanted, part of this volume */}
|
||||||
|
{wanted?.markEntireVolumeWanted ? (
|
||||||
|
<div className="text-sm">sagla volume pahije</div>
|
||||||
|
) : (
|
||||||
|
<div className="my-2">
|
||||||
|
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-0.5 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||||
|
<span className="pr-1 pt-1">
|
||||||
|
<i className="icon-[solar--documents-bold-duotone] w-5 h-5"></i>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className="text-md text-slate-500 dark:text-slate-900">
|
||||||
|
{wanted.issues.length}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* comicVine metadata presence */}
|
||||||
|
{isComicBookMetadataAvailable && (
|
||||||
|
<img
|
||||||
|
src="/src/client/assets/img/cvlogo.svg"
|
||||||
|
alt={"ComicVine metadata detected."}
|
||||||
|
className="inline-block w-6 h-6 md:w-7 md:h-7 flex-shrink-0 object-contain"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!isEmpty(locg) && (
|
||||||
|
<img
|
||||||
|
src="/src/client/assets/img/locglogo.svg"
|
||||||
|
className="w-7 h-7"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
</Card>
|
||||||
{/* issues marked as wanted, part of this volume */}
|
|
||||||
{wanted?.markEntireVolumeWanted ? (
|
|
||||||
<div className="text-sm">sagla volume pahije</div>
|
|
||||||
) : (
|
|
||||||
<div className="my-2">
|
|
||||||
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-0.5 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
|
||||||
<span className="pr-1 pt-1">
|
|
||||||
<i className="icon-[solar--documents-bold-duotone] w-5 h-5"></i>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="text-md text-slate-500 dark:text-slate-900">
|
|
||||||
{wanted.issues.length}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{/* comicVine metadata presence */}
|
);
|
||||||
{isComicBookMetadataAvailable && (
|
},
|
||||||
<img
|
)}
|
||||||
src="/src/client/assets/img/cvlogo.svg"
|
</div>
|
||||||
alt={"ComicVine metadata detected."}
|
</div>
|
||||||
className="w-7 h-7"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{!isEmpty(locg) && (
|
|
||||||
<img
|
|
||||||
src="/src/client/assets/img/locglogo.svg"
|
|
||||||
className="w-7 h-7"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { ReactElement, useCallback, useEffect } from "react";
|
import React, { ReactElement, useCallback, useEffect, useRef } from "react";
|
||||||
import "react-loader-spinner/dist/loader/css/react-spinner-loader.css";
|
import "react-loader-spinner/dist/loader/css/react-spinner-loader.css";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import Loader from "react-loader-spinner";
|
import Loader from "react-loader-spinner";
|
||||||
@@ -27,12 +27,21 @@ interface IProps {
|
|||||||
|
|
||||||
export const Import = (props: IProps): ReactElement => {
|
export const Import = (props: IProps): ReactElement => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { importJobQueue, socketIOInstance } = useStore(
|
const { importJobQueue, getSocket, setQueryClientRef } = useStore(
|
||||||
useShallow((state) => ({
|
useShallow((state) => ({
|
||||||
importJobQueue: state.importJobQueue,
|
importJobQueue: state.importJobQueue,
|
||||||
socketIOInstance: state.socketIOInstance,
|
getSocket: state.getSocket,
|
||||||
|
setQueryClientRef: state.setQueryClientRef,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const previousResultCountRef = useRef<number>(0);
|
||||||
|
const pollingIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
// Set the queryClient reference in the store so socket events can use it
|
||||||
|
useEffect(() => {
|
||||||
|
setQueryClientRef({ current: queryClient });
|
||||||
|
}, [queryClient, setQueryClientRef]);
|
||||||
|
|
||||||
const sessionId = localStorage.getItem("sessionId");
|
const sessionId = localStorage.getItem("sessionId");
|
||||||
const { mutate: initiateImport } = useMutation({
|
const { mutate: initiateImport } = useMutation({
|
||||||
@@ -44,24 +53,91 @@ export const Import = (props: IProps): ReactElement => {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data, isError, isLoading } = useQuery({
|
const { data, isError, isLoading, refetch } = useQuery({
|
||||||
queryKey: ["allImportJobResults"],
|
queryKey: ["allImportJobResults"],
|
||||||
queryFn: async () =>
|
queryFn: async () => {
|
||||||
await axios({
|
const response = await axios({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "http://localhost:3000/api/jobqueue/getJobResultStatistics",
|
url: "http://localhost:3000/api/jobqueue/getJobResultStatistics",
|
||||||
}),
|
params: {
|
||||||
|
_t: Date.now(), // Cache buster
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
'Pragma': 'no-cache',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Track the result count
|
||||||
|
if (response.data?.length) {
|
||||||
|
previousResultCountRef.current = response.data.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
refetchOnMount: true,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
staleTime: 0, // Always consider data stale
|
||||||
|
gcTime: 0, // Don't cache the data (replaces cacheTime in newer versions)
|
||||||
|
// Poll every 5 seconds when import is running
|
||||||
|
refetchInterval: importJobQueue.status === "running" || importJobQueue.status === "paused" ? 5000 : false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Listen for import queue drained event to refresh the table
|
||||||
|
useEffect(() => {
|
||||||
|
const socket = getSocket("/");
|
||||||
|
|
||||||
|
const handleQueueDrained = () => {
|
||||||
|
const initialCount = previousResultCountRef.current;
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = 20; // Poll for up to 20 seconds
|
||||||
|
|
||||||
|
// Clear any existing polling interval
|
||||||
|
if (pollingIntervalRef.current) {
|
||||||
|
clearInterval(pollingIntervalRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Poll every second until we see new data or hit max attempts
|
||||||
|
pollingIntervalRef.current = setInterval(async () => {
|
||||||
|
attempts++;
|
||||||
|
|
||||||
|
const result = await refetch();
|
||||||
|
const newCount = result.data?.data?.length || 0;
|
||||||
|
|
||||||
|
if (newCount > initialCount) {
|
||||||
|
if (pollingIntervalRef.current) {
|
||||||
|
clearInterval(pollingIntervalRef.current);
|
||||||
|
pollingIntervalRef.current = null;
|
||||||
|
}
|
||||||
|
} else if (attempts >= maxAttempts) {
|
||||||
|
if (pollingIntervalRef.current) {
|
||||||
|
clearInterval(pollingIntervalRef.current);
|
||||||
|
pollingIntervalRef.current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.on("LS_IMPORT_QUEUE_DRAINED", handleQueueDrained);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socket.off("LS_IMPORT_QUEUE_DRAINED", handleQueueDrained);
|
||||||
|
if (pollingIntervalRef.current) {
|
||||||
|
clearInterval(pollingIntervalRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [getSocket, queryClient, refetch]);
|
||||||
|
|
||||||
const toggleQueue = (queueAction: string, queueStatus: string) => {
|
const toggleQueue = (queueAction: string, queueStatus: string) => {
|
||||||
socketIOInstance.emit(
|
const socket = getSocket("/");
|
||||||
|
socket.emit(
|
||||||
"call",
|
"call",
|
||||||
"socket.setQueueStatus",
|
"socket.setQueueStatus",
|
||||||
{
|
{
|
||||||
queueAction,
|
queueAction,
|
||||||
queueStatus,
|
queueStatus,
|
||||||
},
|
},
|
||||||
(data) => console.log(data),
|
(data: any) => console.log(data),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
@@ -246,7 +322,7 @@ export const Import = (props: IProps): ReactElement => {
|
|||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody className="divide-y divide-gray-200">
|
<tbody className="divide-y divide-gray-200">
|
||||||
{data?.data.map((jobResult, id) => {
|
{data?.data.map((jobResult: any, id: number) => {
|
||||||
return (
|
return (
|
||||||
<tr key={id}>
|
<tr key={id}>
|
||||||
<td className="whitespace-nowrap px-2 py-2 text-gray-700 dark:text-slate-300">
|
<td className="whitespace-nowrap px-2 py-2 text-gray-700 dark:text-slate-300">
|
||||||
|
|||||||
@@ -152,10 +152,10 @@ export const Library = (): ReactElement => {
|
|||||||
accessorKey: "_source.createdAt",
|
accessorKey: "_source.createdAt",
|
||||||
cell: (info) => {
|
cell: (info) => {
|
||||||
return !isNil(info.getValue()) ? (
|
return !isNil(info.getValue()) ? (
|
||||||
<div className="text-sm w-max ml-3 my-3 text-slate-600 dark:text-slate-900">
|
<span className="inline-flex items-center bg-slate-300 dark:bg-slate-500 text-xs font-medium text-slate-700 dark:text-slate-200 px-3 py-1 rounded-md shadow-sm whitespace-nowrap ml-3 my-3">
|
||||||
<p>{format(parseISO(info.getValue()), "dd MMMM, yyyy")} </p>
|
<i className="icon-[solar--file-download-bold] w-4 h-4 mr-2 opacity-70" />
|
||||||
{format(parseISO(info.getValue()), "h aaaa")}
|
{format(parseISO(info.getValue()), "dd MMM yyyy, h:mm a")}
|
||||||
</div>
|
</span>
|
||||||
) : null;
|
) : null;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -164,23 +164,25 @@ export const Library = (): ReactElement => {
|
|||||||
accessorKey: "_source.acquisition",
|
accessorKey: "_source.acquisition",
|
||||||
cell: (info) => (
|
cell: (info) => (
|
||||||
<div className="flex flex-col gap-2 ml-3 my-3">
|
<div className="flex flex-col gap-2 ml-3 my-3">
|
||||||
<span className="inline-flex items-center w-fit bg-slate-50 text-slate-800 text-xs px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
{/* DC++ Downloads */}
|
||||||
<span className="pr-1 pt-1">
|
{info.getValue().directconnect?.downloads?.length > 0 ? (
|
||||||
<i className="icon-[solar--folder-path-connect-bold-duotone] w-5 h-5"></i>
|
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-1 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||||
|
<i className="icon-[solar--folder-path-connect-bold-duotone] w-4 h-4 mr-1 opacity-70" />
|
||||||
|
<span>
|
||||||
|
DC++: {info.getValue().directconnect.downloads.length}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="text-md text-slate-900 dark:text-slate-900">
|
) : null}
|
||||||
DC++: {info.getValue().directconnect.downloads.length}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="inline-flex w-fit items-center bg-slate-50 text-slate-800 text-xs px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
{/* Torrent Downloads */}
|
||||||
<span className="pr-1 pt-1">
|
{info.getValue().torrent.length > 0 ? (
|
||||||
<i className="icon-[solar--magnet-bold-duotone] w-5 h-5"></i>
|
<span className="inline-flex items-center whitespace-nowrap bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-1 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||||
|
<i className="icon-[solar--magnet-bold-duotone] w-4 h-4 mr-1 opacity-70" />
|
||||||
|
<span className="whitespace-nowrap">
|
||||||
|
Torrent: {info.getValue().torrent.length}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="text-md text-slate-900 dark:text-slate-900">
|
) : null}
|
||||||
Torrent: {info.getValue().torrent.length}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -266,55 +266,80 @@ export const Search = ({}: ISearchProps): ReactElement => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isEmpty(comicVineSearchResults?.data?.results) ? (
|
{!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">
|
<div className="mx-auto w-full sm:w-[90vw] md:w-[80vw] lg:w-[70vw] max-w-6xl px-4 py-6">
|
||||||
{comicVineSearchResults.data.results.map((result) => {
|
{comicVineSearchResults.data.results.map((result) => {
|
||||||
return result.resource_type === "issue" ? (
|
return result.resource_type === "issue" ? (
|
||||||
<div
|
<div
|
||||||
key={result.id}
|
key={result.id}
|
||||||
className="mb-5 dark:bg-slate-400 p-4 rounded-lg"
|
className="relative flex gap-4 py-6 px-3 border-b border-slate-200 dark:border-slate-700 hover:bg-slate-100/50 dark:hover:bg-slate-800/30 transition-colors duration-150 group"
|
||||||
>
|
>
|
||||||
<div className="flex flex-row">
|
{/* IMAGE */}
|
||||||
<div className="mr-5 min-w-[80px] max-w-[13%]">
|
<div className="flex-shrink-0">
|
||||||
<Card
|
<Card
|
||||||
key={result.id}
|
orientation="cover-only"
|
||||||
orientation={"cover-only"}
|
imageUrl={result.image.small_url}
|
||||||
imageUrl={result.image.small_url}
|
hasDetails={false}
|
||||||
hasDetails={false}
|
cardContainerStyle={{ width: "120px", maxWidth: "150px" }}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* RIGHT-SIDE CONTENT */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
{/* TITLE */}
|
||||||
|
<div className="text-base font-medium text-slate-800 dark:text-white tracking-tight truncate">
|
||||||
|
{result.volume?.name || <span>No Name</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-3/4">
|
|
||||||
<div className="text-xl">
|
{/* SUBMETA */}
|
||||||
{!isEmpty(result.volume.name) ? (
|
<div className="flex flex-wrap gap-2 mt-2">
|
||||||
result.volume.name
|
{/* Cover Date Token */}
|
||||||
) : (
|
|
||||||
<span className="is-size-3">No Name</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{result.cover_date && (
|
{result.cover_date && (
|
||||||
<p>
|
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-0.5 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||||
<span className="tag is-light">Cover date</span>
|
<span className="pr-1 pt-1">
|
||||||
{dayjs(result.cover_date).format("MMM D, YYYY")}
|
<i className="icon-[solar--calendar-bold-duotone] w-4 h-4"></i>
|
||||||
</p>
|
</span>
|
||||||
|
<span className="text-xs text-slate-500 dark:text-slate-900">
|
||||||
|
{dayjs(result.cover_date).format("MMM YYYY")}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="tag is-warning">{result.id}</p>
|
{/* ID Token */}
|
||||||
|
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-0.5 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||||
|
<span className="pr-1 pt-1">
|
||||||
|
<i className="icon-[solar--hashtag-bold-duotone] w-4 h-4"></i>
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-slate-500 dark:text-slate-900">
|
||||||
|
{result.id}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<a href={result.api_detail_url}>
|
{/* LINK */}
|
||||||
{result.api_detail_url}
|
<a
|
||||||
</a>
|
href={result.api_detail_url}
|
||||||
<p className="text-sm">
|
className="text-xs text-blue-500 underline mt-1 inline-block break-all"
|
||||||
|
>
|
||||||
|
{result.api_detail_url}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{/* DESCRIPTION */}
|
||||||
|
{result.description && (
|
||||||
|
<p className="text-sm text-slate-600 dark:text-slate-200 mt-2 line-clamp-3">
|
||||||
{ellipsize(
|
{ellipsize(
|
||||||
convert(result.description, {
|
convert(result.description ?? "", {
|
||||||
baseElements: {
|
baseElements: { selectors: ["p", "div"] },
|
||||||
selectors: ["p", "div"],
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
320,
|
300,
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-2">
|
)}
|
||||||
|
|
||||||
|
{/* CTA BUTTON */}
|
||||||
|
{result.volume.name && (
|
||||||
|
<div className="absolute bottom-4 right-4">
|
||||||
<PopoverButton
|
<PopoverButton
|
||||||
content={`This will add ${result.volume.name} to your wanted list.`}
|
content={`This will add ${result?.volume?.name} to your wanted list.`}
|
||||||
clickHandler={() =>
|
clickHandler={() =>
|
||||||
addToWantedList({
|
addToWantedList({
|
||||||
source: "comicvine",
|
source: "comicvine",
|
||||||
@@ -325,114 +350,109 @@ export const Search = ({}: ISearchProps): ReactElement => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
result.resource_type === "volume" && (
|
result.resource_type === "volume" && (
|
||||||
<div
|
<div
|
||||||
key={result.id}
|
key={result.id}
|
||||||
className="mb-5 dark:bg-slate-500 p-4 rounded-lg"
|
className="relative flex gap-4 py-6 px-3 border-b border-slate-200 dark:border-slate-700 hover:bg-slate-100/50 dark:hover:bg-slate-800/30 transition-colors duration-150 group"
|
||||||
>
|
>
|
||||||
<div className="flex flex-row">
|
{/* LEFT COLUMN: COVER */}
|
||||||
<div className="mr-5 min-w-[80px] max-w-[13%]">
|
<Card
|
||||||
<Card
|
orientation="cover-only"
|
||||||
key={result.id}
|
imageUrl={result.image.small_url}
|
||||||
orientation={"cover-only"}
|
hasDetails={false}
|
||||||
imageUrl={result.image.small_url}
|
cardContainerStyle={{
|
||||||
hasDetails={false}
|
width: "120px",
|
||||||
/>
|
maxWidth: "150px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* RIGHT COLUMN */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
{/* TITLE */}
|
||||||
|
<div className="text-lg font-bold text-gray-900 dark:text-white">
|
||||||
|
{result.name || <span>No Name</span>}
|
||||||
|
{result.start_year && <> ({result.start_year})</>}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-3/4">
|
|
||||||
<div className="text-xl">
|
|
||||||
{!isEmpty(result.name) ? (
|
|
||||||
result.name
|
|
||||||
) : (
|
|
||||||
<span className="text-xl">No Name</span>
|
|
||||||
)}
|
|
||||||
{result.start_year && <> ({result.start_year})</>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-row gap-2">
|
{/* TOKENS */}
|
||||||
{/* issue count */}
|
<div className="flex flex-wrap gap-2 mt-2">
|
||||||
{result.count_of_issues && (
|
{/* ISSUE COUNT */}
|
||||||
<div className="my-2">
|
{result.count_of_issues && (
|
||||||
<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="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-0.5 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||||
<span className="pr-1 pt-1">
|
<span className="pr-1 pt-1">
|
||||||
<i className="icon-[solar--documents-minimalistic-bold-duotone] w-5 h-5"></i>
|
<i className="icon-[solar--documents-minimalistic-bold-duotone] w-4 h-4" />
|
||||||
</span>
|
</span>
|
||||||
|
<span>
|
||||||
|
{t("issueWithCount", {
|
||||||
|
count: result.count_of_issues,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
<span className="text-md text-slate-500 dark:text-slate-900">
|
{/* FORMAT DETECTED */}
|
||||||
{t("issueWithCount", {
|
{result.description &&
|
||||||
count: result.count_of_issues,
|
!isEmpty(detectIssueTypes(result.description)) && (
|
||||||
})}
|
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-0.5 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||||
</span>
|
<span className="pr-1 pt-1">
|
||||||
|
<i className="icon-[solar--book-2-line-duotone] w-4 h-4" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
<span>
|
||||||
|
{
|
||||||
|
detectIssueTypes(result.description)
|
||||||
|
.displayName
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
{/* 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">
|
{/* ID */}
|
||||||
{
|
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-2.5 py-0.5 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||||
detectIssueTypes(result.description)
|
<span className="pr-1 pt-1">
|
||||||
.displayName
|
<i className="icon-[solar--hashtag-bold-duotone] w-4 h-4" />
|
||||||
}
|
</span>
|
||||||
</span>
|
<span>{result.id}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span className="tag is-warning">{result.id}</span>
|
{/* LINK */}
|
||||||
<p>
|
<a
|
||||||
<a href={result.api_detail_url}>
|
href={result.api_detail_url}
|
||||||
{result.api_detail_url}
|
className="text-sm text-blue-500 underline mt-2 break-all"
|
||||||
</a>
|
>
|
||||||
</p>
|
{result.api_detail_url}
|
||||||
|
</a>
|
||||||
|
|
||||||
{/* description */}
|
{/* DESCRIPTION */}
|
||||||
<p className="text-sm">
|
{result.description && (
|
||||||
|
<p className="text-sm mt-2 text-slate-700 dark:text-slate-200 break-words line-clamp-3">
|
||||||
{ellipsize(
|
{ellipsize(
|
||||||
convert(result.description, {
|
convert(result.description, {
|
||||||
baseElements: {
|
baseElements: { selectors: ["p", "div"] },
|
||||||
selectors: ["p", "div"],
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
320,
|
320,
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-2">
|
)}
|
||||||
|
{result.name ? (
|
||||||
|
<div className="mt-4 justify-self-end">
|
||||||
<PopoverButton
|
<PopoverButton
|
||||||
content={`Adding this volume will add ${t(
|
content={`This will add ${result.count_of_issues} issues your wanted list.`}
|
||||||
"issueWithCount",
|
|
||||||
{
|
|
||||||
count: result.count_of_issues,
|
|
||||||
},
|
|
||||||
)} to your wanted list.`}
|
|
||||||
clickHandler={() =>
|
clickHandler={() =>
|
||||||
addToWantedList({
|
addToWantedList({
|
||||||
source: "comicvine",
|
source: "comicvine",
|
||||||
comicObject: result,
|
comicObject: result,
|
||||||
markEntireVolumeWanted: true,
|
markEntireVolumeWanted: false,
|
||||||
resourceType: "volume",
|
resourceType: "issue",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { AirDCPPSettingsConfirmation } from "./AirDCPPSettingsConfirmation";
|
import { AirDCPPSettingsConfirmation } from "./AirDCPPSettingsConfirmation";
|
||||||
import { ConnectionForm } from "../../shared/ConnectionForm/ConnectionForm";
|
import { ConnectionForm } from "../../shared/ConnectionForm/ConnectionForm";
|
||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {
|
import {
|
||||||
AIRDCPP_SERVICE_BASE_URI,
|
AIRDCPP_SERVICE_BASE_URI,
|
||||||
|
|||||||
38
src/client/components/Settings/DockerVars/DockerVars.tsx
Normal file
38
src/client/components/Settings/DockerVars/DockerVars.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export const DockerVars = (): React.ReactElement => {
|
||||||
|
const [environmentVariables, setEnvironmentVariables] = React.useState<
|
||||||
|
Record<string, string>
|
||||||
|
>({});
|
||||||
|
const { data } = useQuery({
|
||||||
|
queryKey: ["docker-vars"],
|
||||||
|
queryFn: async () => {
|
||||||
|
await axios({
|
||||||
|
method: "GET",
|
||||||
|
url: "http://localhost:3000/api/settings/getEnvironmentVariables",
|
||||||
|
}).then((response) => {
|
||||||
|
setEnvironmentVariables(response.data);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
|
});
|
||||||
|
console.log("Docker Vars: ", environmentVariables);
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<h2 className="text-xl font-semibold">Docker Environment Variables</h2>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400">
|
||||||
|
<pre>
|
||||||
|
{Object.entries(environmentVariables).length > 0
|
||||||
|
? JSON.stringify(environmentVariables, null, 2)
|
||||||
|
: "No environment variables found."}
|
||||||
|
</pre>
|
||||||
|
</p>
|
||||||
|
{/* Add your form or content for Docker environment variables here */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DockerVars;
|
||||||
@@ -4,6 +4,7 @@ import { AirDCPPHubsForm } from "./AirDCPPSettings/AirDCPPHubsForm";
|
|||||||
import { QbittorrentConnectionForm } from "./QbittorrentSettings/QbittorrentConnectionForm";
|
import { QbittorrentConnectionForm } from "./QbittorrentSettings/QbittorrentConnectionForm";
|
||||||
import { SystemSettingsForm } from "./SystemSettings/SystemSettingsForm";
|
import { SystemSettingsForm } from "./SystemSettings/SystemSettingsForm";
|
||||||
import ProwlarrSettingsForm from "./ProwlarrSettings/ProwlarrSettingsForm";
|
import ProwlarrSettingsForm from "./ProwlarrSettings/ProwlarrSettingsForm";
|
||||||
|
import DockerVars from "./DockerVars/DockerVars";
|
||||||
import { ServiceStatuses } from "../ServiceStatuses/ServiceStatuses";
|
import { ServiceStatuses } from "../ServiceStatuses/ServiceStatuses";
|
||||||
import settingsObject from "../../constants/settings/settingsMenu.json";
|
import settingsObject from "../../constants/settings/settingsMenu.json";
|
||||||
import { isUndefined, map } from "lodash";
|
import { isUndefined, map } from "lodash";
|
||||||
@@ -12,139 +13,130 @@ interface ISettingsProps {}
|
|||||||
|
|
||||||
export const Settings = (props: ISettingsProps): ReactElement => {
|
export const Settings = (props: ISettingsProps): ReactElement => {
|
||||||
const [active, setActive] = useState("gen-db");
|
const [active, setActive] = useState("gen-db");
|
||||||
console.log(active);
|
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
const toggleExpanded = (id: string) => {
|
||||||
|
setExpanded((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[id]: !prev[id],
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
const settingsContent = [
|
const settingsContent = [
|
||||||
{
|
{ id: "adc-hubs", content: <AirDCPPHubsForm /> },
|
||||||
id: "adc-hubs",
|
{ id: "adc-connection", content: <AirDCPPSettingsForm /> },
|
||||||
content: (
|
{id: "gen-docker-vars", content: <DockerVars />},
|
||||||
<div key="adc-hubs">
|
{ id: "qbt-connection", content: <QbittorrentConnectionForm /> },
|
||||||
<AirDCPPHubsForm />
|
{ id: "prwlr-connection", content: <ProwlarrSettingsForm /> },
|
||||||
</div>
|
{ id: "core-service", content: <>a</> },
|
||||||
),
|
{ id: "flushdb", content: <SystemSettingsForm /> },
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "adc-connection",
|
|
||||||
content: (
|
|
||||||
<div key="adc-connection">
|
|
||||||
<AirDCPPSettingsForm />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "qbt-connection",
|
|
||||||
content: (
|
|
||||||
<div key="qbt-connection">
|
|
||||||
<QbittorrentConnectionForm />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "prwlr-connection",
|
|
||||||
content: (
|
|
||||||
<>
|
|
||||||
<ProwlarrSettingsForm />
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "core-service",
|
|
||||||
content: <>a</>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "flushdb",
|
|
||||||
content: (
|
|
||||||
<div key="flushdb">
|
|
||||||
<SystemSettingsForm />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<section>
|
<section>
|
||||||
|
{/* Header */}
|
||||||
<header className="bg-slate-200 dark:bg-slate-500">
|
<header className="bg-slate-200 dark:bg-slate-500">
|
||||||
<div className="mx-auto max-w-screen-xl px-2 py-2 sm:px-6 sm:py-8 lg:px-8 lg:py-4">
|
<div className="mx-auto max-w-screen-xl px-4 py-6 sm:px-6 lg:px-8">
|
||||||
<div className="sm:flex sm:items-center sm:justify-between">
|
<div className="sm:flex sm:items-center sm:justify-between">
|
||||||
<div className="text-center sm:text-left">
|
<div className="text-center sm:text-left">
|
||||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white sm:text-3xl">
|
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-white">
|
||||||
Settings
|
Settings
|
||||||
</h1>
|
</h1>
|
||||||
|
<p className="mt-1 text-sm text-gray-500 dark:text-white">
|
||||||
<p className="mt-1.5 text-sm text-gray-500 dark:text-white">
|
|
||||||
Import comics into the ThreeTwo library.
|
Import comics into the ThreeTwo library.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div className="flex flex-row">
|
|
||||||
<div className="inset-y-0 w-80 dark:bg-gray-800 bg-slate-300 text-white overflow-y-auto">
|
{/* Main Layout */}
|
||||||
<aside className="px-4 py-4 sm:px-6 sm:py-8 lg:px-8">
|
<div className="flex gap-8 px-12 py-6">
|
||||||
{map(settingsObject, (settingObject, idx) => {
|
{/* Sidebar */}
|
||||||
return (
|
<div className="relative z-30">
|
||||||
|
<aside
|
||||||
|
className="sticky top-6 w-72 max-h-[90vh]
|
||||||
|
rounded-2xl shadow-xl backdrop-blur-md
|
||||||
|
bg-white/70 dark:bg-slate-800/60
|
||||||
|
border border-slate-200 dark:border-slate-700
|
||||||
|
overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="px-4 py-6 overflow-y-auto">
|
||||||
|
{map(settingsObject, (settingObject, idx) => (
|
||||||
<div
|
<div
|
||||||
className="w-64 py-2 text-slate-700 dark:text-slate-400"
|
|
||||||
key={idx}
|
key={idx}
|
||||||
|
className="mb-6 text-slate-700 dark:text-slate-300"
|
||||||
>
|
>
|
||||||
<h3 className="text-l pb-2">
|
<h3 className="text-xs font-semibold text-slate-500 dark:text-slate-400 tracking-wide mb-3">
|
||||||
{settingObject.category.toUpperCase()}
|
{settingObject.category.toUpperCase()}
|
||||||
</h3>
|
</h3>
|
||||||
{/* First level children */}
|
|
||||||
{!isUndefined(settingObject.children) ? (
|
{!isUndefined(settingObject.children) && (
|
||||||
<ul key={settingObject.id}>
|
<ul>
|
||||||
{map(settingObject.children, (item, idx) => {
|
{map(settingObject.children, (item, idx) => {
|
||||||
|
const isOpen = expanded[item.id];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={idx} className="mb-2">
|
<li key={idx} className="mb-1">
|
||||||
<a
|
<div
|
||||||
className={
|
onClick={() => toggleExpanded(item.id)}
|
||||||
item.id.toString() === active
|
className={`cursor-pointer flex justify-between items-center px-1 py-1 rounded-md transition-colors hover:bg-white/50 dark:hover:bg-slate-700 ${
|
||||||
? "is-active flex items-center"
|
item.id === active
|
||||||
: "flex items-center"
|
? "font-semibold text-blue-600 dark:text-blue-400"
|
||||||
}
|
: ""
|
||||||
onClick={() => setActive(item.id.toString())}
|
}`}
|
||||||
>
|
>
|
||||||
{item.displayName}
|
<span
|
||||||
</a>
|
onClick={() => setActive(item.id.toString())}
|
||||||
{/* Second level children */}
|
className="flex-1"
|
||||||
{!isUndefined(item.children) ? (
|
>
|
||||||
<ul className="pl-4 mt-2">
|
{item.displayName}
|
||||||
{map(item.children, (item, idx) => (
|
</span>
|
||||||
<li key={item.id} className="mb-2">
|
{!isUndefined(item.children) && (
|
||||||
|
<span className="text-xs opacity-60">
|
||||||
|
{isOpen ? "−" : "+"}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!isUndefined(item.children) && isOpen && (
|
||||||
|
<ul className="pl-4 mt-1">
|
||||||
|
{map(item.children, (subItem) => (
|
||||||
|
<li key={subItem.id} className="mb-1">
|
||||||
<a
|
<a
|
||||||
className={
|
|
||||||
item.id.toString() === active
|
|
||||||
? "is-active flex items-center"
|
|
||||||
: "flex items-center"
|
|
||||||
}
|
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setActive(item.id.toString())
|
setActive(subItem.id.toString())
|
||||||
}
|
}
|
||||||
|
className={`cursor-pointer flex items-center px-1 py-1 rounded-md transition-colors hover:bg-white/50 dark:hover:bg-slate-700 ${
|
||||||
|
subItem.id.toString() === active
|
||||||
|
? "font-semibold text-blue-600 dark:text-blue-400"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{item.displayName}
|
{subItem.displayName}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
) : null}
|
)}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
) : null}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
))}
|
||||||
})}
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* content for settings */}
|
{/* Content */}
|
||||||
<div className="flex mx-12">
|
<main className="flex-1 px-2 py-2">
|
||||||
<div className="">
|
{settingsContent.map(({ id, content }) =>
|
||||||
{map(settingsContent, ({ id, content }) =>
|
active === id ? <div key={id}>{content}</div> : null,
|
||||||
active === id ? content : null,
|
)}
|
||||||
)}
|
</main>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,37 +1,87 @@
|
|||||||
import React, { ReactElement, useCallback, useEffect, useMemo } from "react";
|
import React from "react";
|
||||||
import SearchBar from "../Library/SearchBar";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { gql, GraphQLClient } from "graphql-request";
|
||||||
import T2Table from "../shared/T2Table";
|
import T2Table from "../shared/T2Table";
|
||||||
import MetadataPanel from "../shared/MetadataPanel";
|
import MetadataPanel from "../shared/MetadataPanel";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import axios from "axios";
|
|
||||||
import { SEARCH_SERVICE_BASE_URI } from "../../constants/endpoints";
|
|
||||||
|
|
||||||
export const WantedComics = (props): ReactElement => {
|
/**
|
||||||
const {
|
* GraphQL client for interfacing with Moleculer Apollo server.
|
||||||
data: wantedComics,
|
*/
|
||||||
isSuccess,
|
const client = new GraphQLClient("http://localhost:3000/graphql");
|
||||||
isFetched,
|
|
||||||
isError,
|
|
||||||
isLoading,
|
|
||||||
} = useQuery({
|
|
||||||
queryFn: async () =>
|
|
||||||
await axios({
|
|
||||||
url: `${SEARCH_SERVICE_BASE_URI}/searchIssue`,
|
|
||||||
method: "POST",
|
|
||||||
data: {
|
|
||||||
query: {},
|
|
||||||
|
|
||||||
pagination: {
|
/**
|
||||||
size: 25,
|
* GraphQL query to fetch wanted comics.
|
||||||
from: 0,
|
*/
|
||||||
},
|
const WANTED_COMICS_QUERY = gql`
|
||||||
type: "wanted",
|
query {
|
||||||
trigger: "wantedComicsPage",
|
wantedComics(limit: 25, offset: 0) {
|
||||||
},
|
total
|
||||||
}),
|
comics
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shape of an individual comic returned by the backend.
|
||||||
|
*/
|
||||||
|
type Comic = {
|
||||||
|
_id: string;
|
||||||
|
sourcedMetadata?: {
|
||||||
|
comicvine?: {
|
||||||
|
name?: string;
|
||||||
|
start_year?: string;
|
||||||
|
publisher?: {
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
acquisition?: {
|
||||||
|
directconnect?: {
|
||||||
|
downloads?: Array<{
|
||||||
|
name: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shape of the GraphQL response returned for wanted comics.
|
||||||
|
*/
|
||||||
|
type WantedComicsResponse = {
|
||||||
|
wantedComics: {
|
||||||
|
total: number;
|
||||||
|
comics: Comic[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React component rendering the "Wanted Comics" table using T2Table.
|
||||||
|
* Fetches data from GraphQL backend via graphql-request + TanStack Query.
|
||||||
|
*
|
||||||
|
* @component
|
||||||
|
* @returns {JSX.Element} React component
|
||||||
|
*/
|
||||||
|
const WantedComics = (): JSX.Element => {
|
||||||
|
const { data, isLoading, isError, isSuccess, error } = useQuery<
|
||||||
|
WantedComicsResponse["wantedComics"]
|
||||||
|
>({
|
||||||
queryKey: ["wantedComics"],
|
queryKey: ["wantedComics"],
|
||||||
enabled: true,
|
queryFn: async () => {
|
||||||
|
const res = await client.request<WantedComicsResponse>(
|
||||||
|
WANTED_COMICS_QUERY,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!res?.wantedComics?.comics) {
|
||||||
|
throw new Error("No comics returned");
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.wantedComics;
|
||||||
|
},
|
||||||
|
retry: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const columnData = [
|
const columnData = [
|
||||||
{
|
{
|
||||||
header: "Comic Information",
|
header: "Comic Information",
|
||||||
@@ -40,11 +90,11 @@ export const WantedComics = (props): ReactElement => {
|
|||||||
header: "Details",
|
header: "Details",
|
||||||
id: "comicDetails",
|
id: "comicDetails",
|
||||||
minWidth: 350,
|
minWidth: 350,
|
||||||
accessorFn: (data) => data,
|
accessorFn: (data: Comic) => data,
|
||||||
cell: (value) => {
|
cell: (value: any) => {
|
||||||
console.log("ASDASd", value);
|
const row = value.getValue();
|
||||||
const row = value.getValue()._source;
|
console.log("Comic row data:", row);
|
||||||
return row && <MetadataPanel data={row} />;
|
return row ? <MetadataPanel data={row} /> : null;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -55,148 +105,73 @@ export const WantedComics = (props): ReactElement => {
|
|||||||
{
|
{
|
||||||
header: "Files",
|
header: "Files",
|
||||||
align: "right",
|
align: "right",
|
||||||
accessorKey: "_source.acquisition",
|
accessorFn: (row: Comic) =>
|
||||||
cell: (props) => {
|
row?.acquisition?.directconnect?.downloads || [],
|
||||||
const {
|
cell: (props: any) => {
|
||||||
directconnect: { downloads },
|
const downloads = props.getValue();
|
||||||
} = props.getValue();
|
return downloads?.length > 0 ? (
|
||||||
return (
|
<span className="tag is-warning">{downloads.length}</span>
|
||||||
<div
|
) : null;
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
// flexDirection: "column",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{downloads.length > 0 ? (
|
|
||||||
<span className="tag is-warning">{downloads.length}</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: "Download Details",
|
header: "Download Details",
|
||||||
id: "downloadDetails",
|
id: "downloadDetails",
|
||||||
accessorKey: "_source.acquisition",
|
accessorFn: (row: Comic) =>
|
||||||
cell: (data) => (
|
row?.acquisition?.directconnect?.downloads || [],
|
||||||
|
cell: (data: any) => (
|
||||||
<ol>
|
<ol>
|
||||||
{data.getValue().directconnect.downloads.map((download, idx) => {
|
{data.getValue()?.map((download: any, idx: number) => (
|
||||||
return (
|
<li className="is-size-7" key={idx}>
|
||||||
<li className="is-size-7" key={idx}>
|
{download.name}
|
||||||
{download.name}
|
</li>
|
||||||
</li>
|
))}
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ol>
|
</ol>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
header: "Type",
|
|
||||||
id: "dcc",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
|
||||||
* Pagination control that fetches the next x (pageSize) items
|
|
||||||
* based on the y (pageIndex) offset from the Elasticsearch index
|
|
||||||
* @param {number} pageIndex
|
|
||||||
* @param {number} pageSize
|
|
||||||
* @returns void
|
|
||||||
*
|
|
||||||
**/
|
|
||||||
// const nextPage = useCallback((pageIndex: number, pageSize: number) => {
|
|
||||||
// dispatch(
|
|
||||||
// searchIssue(
|
|
||||||
// {
|
|
||||||
// query: {},
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// pagination: {
|
|
||||||
// size: pageSize,
|
|
||||||
// from: pageSize * pageIndex + 1,
|
|
||||||
// },
|
|
||||||
// type: "wanted",
|
|
||||||
// trigger: "wantedComicsPage",
|
|
||||||
// },
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// }, []);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pagination control that fetches the previous x (pageSize) items
|
|
||||||
* based on the y (pageIndex) offset from the Elasticsearch index
|
|
||||||
* @param {number} pageIndex
|
|
||||||
* @param {number} pageSize
|
|
||||||
* @returns void
|
|
||||||
**/
|
|
||||||
// const previousPage = useCallback((pageIndex: number, pageSize: number) => {
|
|
||||||
// let from = 0;
|
|
||||||
// if (pageIndex === 2) {
|
|
||||||
// from = (pageIndex - 1) * pageSize + 2 - 17;
|
|
||||||
// } else {
|
|
||||||
// from = (pageIndex - 1) * pageSize + 2 - 16;
|
|
||||||
// }
|
|
||||||
// dispatch(
|
|
||||||
// searchIssue(
|
|
||||||
// {
|
|
||||||
// query: {},
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// pagination: {
|
|
||||||
// size: pageSize,
|
|
||||||
// from,
|
|
||||||
// },
|
|
||||||
// type: "wanted",
|
|
||||||
// trigger: "wantedComicsPage",
|
|
||||||
// },
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// }, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="">
|
<section>
|
||||||
<section className="">
|
<header className="bg-slate-200 dark:bg-slate-500">
|
||||||
<header className="bg-slate-200 dark:bg-slate-500">
|
<div className="mx-auto max-w-screen-xl px-2 py-2 sm:px-6 sm:py-8 lg:px-8 lg:py-4">
|
||||||
<div className="mx-auto max-w-screen-xl px-2 py-2 sm:px-6 sm:py-8 lg:px-8 lg:py-4">
|
<div className="sm:flex sm:items-center sm:justify-between">
|
||||||
<div className="sm:flex sm:items-center sm:justify-between">
|
<div className="text-center sm:text-left">
|
||||||
<div className="text-center sm:text-left">
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white sm:text-3xl">
|
||||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white sm:text-3xl">
|
Wanted Comics
|
||||||
Wanted Comics
|
</h1>
|
||||||
</h1>
|
<p className="mt-1.5 text-sm text-gray-500 dark:text-white">
|
||||||
|
Browse through comics you marked as "wanted."
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<p className="mt-1.5 text-sm text-gray-500 dark:text-white">
|
{isLoading && (
|
||||||
Browse through comics you marked as "wanted."
|
<div className="animate-pulse p-4 space-y-4">
|
||||||
</p>
|
{Array.from({ length: 5 }).map((_, idx) => (
|
||||||
</div>
|
<div
|
||||||
</div>
|
key={idx}
|
||||||
</div>
|
className="h-24 bg-slate-300 dark:bg-slate-600 rounded-md"
|
||||||
</header>
|
/>
|
||||||
{isSuccess && wantedComics?.data.hits?.hits ? (
|
))}
|
||||||
<div>
|
</div>
|
||||||
<div className="library">
|
)}
|
||||||
<T2Table
|
{isError && <div>Error fetching wanted comics. {error?.message}</div>}
|
||||||
sourceData={wantedComics?.data.hits.hits}
|
{isSuccess && data?.comics?.length > 0 ? (
|
||||||
totalPages={wantedComics?.data.hits.hits.length}
|
<T2Table
|
||||||
columns={columnData}
|
sourceData={data.comics}
|
||||||
paginationHandlers={{
|
totalPages={data.comics.length}
|
||||||
nextPage: () => {},
|
columns={columnData}
|
||||||
previousPage: () => {},
|
paginationHandlers={{}}
|
||||||
}}
|
/>
|
||||||
// rowClickHandler={navigateToComicDetail}
|
) : isSuccess ? (
|
||||||
/>
|
<div>No comics found.</div>
|
||||||
{/* pagination controls */}
|
) : null}
|
||||||
</div>
|
</section>
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{isLoading ? <div>Loading...</div> : null}
|
|
||||||
{isError ? (
|
|
||||||
<div>An error occurred while retrieving the pull list.</div>
|
|
||||||
) : null}
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ interface ICardProps {
|
|||||||
borderColorClass?: string;
|
borderColorClass?: string;
|
||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
|
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
|
||||||
cardContainerStyle?: PropTypes.object;
|
cardContainerStyle?: any;
|
||||||
imageStyle?: PropTypes.object;
|
imageStyle?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderCard = (props: ICardProps): ReactElement => {
|
const renderCard = (props: ICardProps): ReactElement => {
|
||||||
@@ -83,11 +83,11 @@ const renderCard = (props: ICardProps): ReactElement => {
|
|||||||
|
|
||||||
case "vertical-2":
|
case "vertical-2":
|
||||||
return (
|
return (
|
||||||
<div className="block rounded-md w-64 h-fit shadow-md shadow-white-400 bg-gray-200 dark:bg-slate-500">
|
<div className="block rounded-md max-w-64 h-fit shadow-md shadow-white-400 bg-gray-200 dark:bg-slate-500">
|
||||||
<img
|
<img
|
||||||
alt="Home"
|
alt="Home"
|
||||||
src={props.imageUrl}
|
src={props.imageUrl}
|
||||||
className="rounded-t-md object-cover"
|
className="rounded-t-md object-cover w-full"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{props.title ? (
|
{props.title ? (
|
||||||
@@ -140,14 +140,31 @@ const renderCard = (props: ICardProps): ReactElement => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
case "cover-only":
|
case "cover-only":
|
||||||
|
const containerStyle = {
|
||||||
|
width: props.cardContainerStyle?.width || "100%",
|
||||||
|
height: props.cardContainerStyle?.height || "auto",
|
||||||
|
maxWidth: props.cardContainerStyle?.maxWidth || "none",
|
||||||
|
...props.cardContainerStyle,
|
||||||
|
};
|
||||||
|
|
||||||
|
const imageStyle = {
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
objectFit: "cover",
|
||||||
|
...props.imageStyle,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div
|
||||||
{/* thumbnail */}
|
className={`rounded-2xl overflow-hidden shadow-md bg-white dark:bg-slate-800 ${
|
||||||
<div className="rounded-lg shadow-lg overflow-hidden w-fit h-fit">
|
props.cardContainerStyle?.height ? "" : "aspect-[2/3]"
|
||||||
<img src={props.imageUrl} />
|
}`}
|
||||||
</div>
|
style={containerStyle}
|
||||||
</>
|
>
|
||||||
|
<img src={props.imageUrl} alt="Comic cover" style={imageStyle} />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
case "card-with-info-panel":
|
case "card-with-info-panel":
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const DatePickerDialog = (props) => {
|
|||||||
const handleDaySelect = (date) => {
|
const handleDaySelect = (date) => {
|
||||||
setSelected(date);
|
setSelected(date);
|
||||||
if (date) {
|
if (date) {
|
||||||
setter(format(date, "yyyy-MM-dd"));
|
setter(format(date, "yyyy/MM/dd"));
|
||||||
apiAction();
|
apiAction();
|
||||||
closePopper();
|
closePopper();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React, { ReactElement } from "react";
|
import React, { ReactElement } from "react";
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import ellipsize from "ellipsize";
|
import ellipsize from "ellipsize";
|
||||||
import prettyBytes from "pretty-bytes";
|
import prettyBytes from "pretty-bytes";
|
||||||
import { Card } from "../shared/Carda";
|
import { Card } from "../shared/Carda";
|
||||||
@@ -7,26 +6,48 @@ import { convert } from "html-to-text";
|
|||||||
import { determineCoverFile } from "../../shared/utils/metadata.utils";
|
import { determineCoverFile } from "../../shared/utils/metadata.utils";
|
||||||
import { find, isUndefined } from "lodash";
|
import { find, isUndefined } from "lodash";
|
||||||
|
|
||||||
interface IMetadatPanelProps {
|
/**
|
||||||
value: any;
|
* Props for the MetadataPanel component.
|
||||||
children: any;
|
*/
|
||||||
imageStyle: any;
|
interface MetadataPanelProps {
|
||||||
titleStyle: any;
|
/**
|
||||||
tagsStyle: any;
|
* Comic metadata object passed into the panel.
|
||||||
containerStyle: any;
|
*/
|
||||||
|
data: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional custom styling for the cover image.
|
||||||
|
*/
|
||||||
|
imageStyle?: React.CSSProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional custom styling for the title section.
|
||||||
|
*/
|
||||||
|
titleStyle?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => {
|
|
||||||
|
/**
|
||||||
|
* MetadataPanel component
|
||||||
|
*
|
||||||
|
* Displays structured comic metadata based on the best available source
|
||||||
|
* (raw file data, ComicVine, or League of Comic Geeks).
|
||||||
|
*
|
||||||
|
* @component
|
||||||
|
* @param {MetadataPanelProps} props
|
||||||
|
* @returns {ReactElement}
|
||||||
|
*/
|
||||||
|
export const MetadataPanel = (props: MetadataPanelProps): ReactElement => {
|
||||||
const {
|
const {
|
||||||
rawFileDetails,
|
rawFileDetails,
|
||||||
inferredMetadata,
|
inferredMetadata,
|
||||||
sourcedMetadata: { comicvine, locg },
|
sourcedMetadata: { comicvine, locg },
|
||||||
} = props.data;
|
} = props.data;
|
||||||
|
|
||||||
const { issueName, url, objectReference } = determineCoverFile({
|
const { issueName, url, objectReference } = determineCoverFile({
|
||||||
comicvine,
|
comicvine,
|
||||||
locg,
|
locg,
|
||||||
rawFileDetails,
|
rawFileDetails,
|
||||||
});
|
});
|
||||||
|
|
||||||
const metadataContentPanel = [
|
const metadataContentPanel = [
|
||||||
{
|
{
|
||||||
name: "rawFileDetails",
|
name: "rawFileDetails",
|
||||||
@@ -43,48 +64,29 @@ export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => {
|
|||||||
</span>
|
</span>
|
||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
{/* Issue number */}
|
|
||||||
{inferredMetadata.issue.number && (
|
{inferredMetadata.issue.number && (
|
||||||
<dd className="my-2">
|
<dd 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="inline-flex items-center bg-slate-100 dark:bg-slate-400 text-slate-800 dark:text-slate-900 text-xs font-medium px-2 py-1 rounded-md">
|
||||||
<span className="pr-1 pt-1">
|
<i className="icon-[solar--hashtag-bold-duotone] w-4 h-4 mr-1 opacity-70"></i>
|
||||||
<i className="icon-[solar--hashtag-outline] w-3.5 h-3.5"></i>
|
<span>{inferredMetadata.issue.number}</span>
|
||||||
</span>
|
|
||||||
<span className="text-md text-slate-900 dark:text-slate-900">
|
|
||||||
{inferredMetadata.issue.number}
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</dd>
|
</dd>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<dd className="flex flex-row gap-2 w-max">
|
<dd className="flex flex-row gap-2 w-max">
|
||||||
{/* File extension */}
|
<span className="inline-flex items-center bg-slate-100 dark:bg-slate-400 text-slate-800 dark:text-slate-900 text-xs font-medium px-2 py-1 rounded-md">
|
||||||
<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">
|
<i className="icon-[solar--file-text-bold-duotone] w-4 h-4 mr-1 opacity-70" />
|
||||||
<span className="pr-1 pt-1">
|
{rawFileDetails.mimeType}
|
||||||
<i className="icon-[solar--zip-file-bold-duotone] w-5 h-5"></i>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="text-md text-slate-500 dark:text-slate-900">
|
|
||||||
{rawFileDetails.mimeType}
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{/* size */}
|
<span className="inline-flex items-center bg-slate-100 dark:bg-slate-400 text-slate-800 dark:text-slate-900 text-xs font-medium px-2 py-1 rounded-md">
|
||||||
<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">
|
<i className="icon-[solar--database-bold-duotone] w-4 h-4 mr-1 opacity-70" />
|
||||||
<span className="pr-1 pt-1">
|
{prettyBytes(rawFileDetails.fileSize)}
|
||||||
<i className="icon-[solar--mirror-right-bold-duotone] w-5 h-5"></i>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="text-md text-slate-500 dark:text-slate-900">
|
|
||||||
{prettyBytes(rawFileDetails.fileSize)}
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{/* Uncompressed version available? */}
|
|
||||||
{rawFileDetails.archive?.uncompressed && (
|
{rawFileDetails.archive?.uncompressed && (
|
||||||
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
|
||||||
<span className="pr-1 pt-1">
|
<i className="icon-[solar--bookmark-bold-duotone] w-3.5 h-3.5 pr-1 pt-1" />
|
||||||
<i className="icon-[solar--bookmark-bold-duotone] w-3.5 h-3.5"></i>
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</dd>
|
</dd>
|
||||||
@@ -94,49 +96,56 @@ export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => {
|
|||||||
|
|
||||||
{
|
{
|
||||||
name: "comicvine",
|
name: "comicvine",
|
||||||
content: () =>
|
content: () => {
|
||||||
!isUndefined(comicvine) &&
|
return (
|
||||||
!isUndefined(comicvine.volumeInformation) && (
|
!isUndefined(comicvine?.volumeInformation) && (
|
||||||
<dl>
|
<dl className="space-y-1 text-sm text-slate-700 dark:text-slate-200">
|
||||||
<dt>
|
{/* Title */}
|
||||||
<h6
|
<dt className="text-base font-semibold text-slate-900 dark:text-white">
|
||||||
className="name has-text-weight-medium mb-1"
|
{ellipsize(issueName, 28)}
|
||||||
style={props.titleStyle}
|
</dt>
|
||||||
>
|
|
||||||
{ellipsize(issueName, 18)}
|
{/* Volume Name */}
|
||||||
</h6>
|
<dd>
|
||||||
</dt>
|
<span className="text-sm text-slate-600 dark:text-slate-300">
|
||||||
<dd>
|
Part of{" "}
|
||||||
<span className="is-size-7">
|
<span className="font-medium text-slate-800 dark:text-white">
|
||||||
Is a part of{" "}
|
{comicvine.volumeInformation.name}
|
||||||
<span className="has-text-weight-semibold">
|
</span>
|
||||||
{comicvine.volumeInformation.name}
|
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</dd>
|
||||||
</dd>
|
|
||||||
<dd className="is-size-7">
|
{/* Description */}
|
||||||
<span>
|
<dd className="text-slate-600 dark:text-slate-300">
|
||||||
{ellipsize(
|
{ellipsize(
|
||||||
convert(comicvine.description, {
|
convert(comicvine.description || "", {
|
||||||
baseElements: {
|
baseElements: { selectors: ["p"] },
|
||||||
selectors: ["p"],
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
120,
|
160,
|
||||||
)}
|
)}
|
||||||
</span>
|
</dd>
|
||||||
</dd>
|
|
||||||
<dd className="is-size-7 mt-2">
|
{/* Misc Info */}
|
||||||
<span className="my-3 mx-2">
|
<dd className="flex flex-wrap items-center gap-2 pt-2 text-xs text-slate-500 dark:text-slate-300">
|
||||||
{comicvine.volumeInformation.start_year}
|
<span className="inline-flex items-center bg-slate-100 dark:bg-slate-600 px-2 py-0.5 rounded-md">
|
||||||
</span>
|
<i className="icon-[solar--calendar-bold-duotone] w-4 h-4 mr-1 opacity-70" />
|
||||||
{comicvine.volumeInformation.count_of_issues}
|
{comicvine.volumeInformation.start_year}
|
||||||
ComicVine ID
|
</span>
|
||||||
{comicvine.id}
|
<span className="inline-flex items-center bg-slate-100 dark:bg-slate-600 px-2 py-0.5 rounded-md">
|
||||||
</dd>
|
<i className="icon-[solar--book-bold-duotone] w-4 h-4 mr-1 opacity-70" />
|
||||||
</dl>
|
{comicvine.volumeInformation.count_of_issues} issues
|
||||||
),
|
</span>
|
||||||
|
<span className="inline-flex items-center bg-slate-100 dark:bg-slate-600 px-2 py-0.5 rounded-md">
|
||||||
|
<i className="icon-[solar--hashtag-bold-duotone] w-4 h-4 mr-1 opacity-70" />
|
||||||
|
ID: {comicvine.id}
|
||||||
|
</span>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: "locg",
|
name: "locg",
|
||||||
content: () => (
|
content: () => (
|
||||||
@@ -147,23 +156,22 @@ export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => {
|
|||||||
</h6>
|
</h6>
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="is-size-7">
|
<dd className="is-size-7">
|
||||||
<span>{ellipsize(locg.description, 120)}</span>
|
<span>{ellipsize(locg?.description || "", 120)}</span>
|
||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
<dd className="is-size-7 mt-2">
|
<dd className="is-size-7 mt-2">
|
||||||
<div className="field is-grouped is-grouped-multiline">
|
<div className="field is-grouped is-grouped-multiline">
|
||||||
<div className="control">
|
<div className="control">
|
||||||
<span className="tags">
|
<span className="tags">
|
||||||
<span className="tag is-success is-light has-text-weight-semibold">
|
<span className="tag is-success is-light has-text-weight-semibold">
|
||||||
{locg.price}
|
{locg?.price}
|
||||||
</span>
|
</span>
|
||||||
<span className="tag is-success is-light">{locg.pulls}</span>
|
<span className="tag is-success is-light">{locg?.pulls}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="control">
|
<div className="control">
|
||||||
<div className="tags has-addons">
|
<div className="tags has-addons">
|
||||||
<span className="tag is-primary is-light">rating</span>
|
<span className="tag is-primary is-light">rating</span>
|
||||||
<span className="tag is-info is-light">{locg.rating}</span>
|
<span className="tag is-info is-light">{locg?.rating}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -173,20 +181,19 @@ export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Find the panel to display
|
|
||||||
const metadataPanel = find(metadataContentPanel, {
|
const metadataPanel = find(metadataContentPanel, {
|
||||||
name: objectReference,
|
name: objectReference,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-5 my-3">
|
<div className="flex gap-5 my-3">
|
||||||
<Card
|
<Card
|
||||||
imageUrl={url}
|
imageUrl={url}
|
||||||
orientation={"cover-only"}
|
orientation="cover-only"
|
||||||
hasDetails={false}
|
hasDetails={false}
|
||||||
imageStyle={props.imageStyle}
|
imageStyle={props.imageStyle}
|
||||||
|
cardContainerStyle={{ width: "190px", maxWidth: "230px" }}
|
||||||
/>
|
/>
|
||||||
<div>{metadataPanel.content()}</div>
|
<div>{metadataPanel?.content()}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const PopoverButton = ({ content, clickHandler }) => {
|
|||||||
onFocus={() => setIsVisible(true)}
|
onFocus={() => setIsVisible(true)}
|
||||||
onBlur={() => setIsVisible(false)}
|
onBlur={() => setIsVisible(false)}
|
||||||
aria-describedby="popover"
|
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"
|
className="flex text-sm 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-1.5 py-1 text-gray-500 hover:bg-transparent hover:text-green-600 focus:outline-none focus:ring active:text-indigo-500"
|
||||||
onClick={clickHandler}
|
onClick={clickHandler}
|
||||||
>
|
>
|
||||||
<i className="icon-[solar--add-square-bold-duotone] w-6 h-6 mr-2"></i>{" "}
|
<i className="icon-[solar--add-square-bold-duotone] w-6 h-6 mr-2"></i>{" "}
|
||||||
@@ -32,7 +32,7 @@ const PopoverButton = ({ content, clickHandler }) => {
|
|||||||
<div
|
<div
|
||||||
ref={refs.setFloating} // Apply the floating setter directly to the ref prop
|
ref={refs.setFloating} // Apply the floating setter directly to the ref prop
|
||||||
style={floatingStyles}
|
style={floatingStyles}
|
||||||
className="text-sm bg-slate-400 p-2 rounded-md"
|
className="text-xs bg-slate-400 p-1.5 rounded-md"
|
||||||
role="tooltip"
|
role="tooltip"
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React, { ReactElement, useMemo, useState } from "react";
|
import React, { ReactElement, useMemo, useState } from "react";
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import {
|
import {
|
||||||
ColumnDef,
|
ColumnDef,
|
||||||
flexRender,
|
flexRender,
|
||||||
@@ -9,7 +8,19 @@ import {
|
|||||||
PaginationState,
|
PaginationState,
|
||||||
} from "@tanstack/react-table";
|
} from "@tanstack/react-table";
|
||||||
|
|
||||||
export const T2Table = (tableOptions): ReactElement => {
|
interface T2TableProps {
|
||||||
|
sourceData?: unknown[];
|
||||||
|
totalPages?: number;
|
||||||
|
columns?: unknown[];
|
||||||
|
paginationHandlers?: {
|
||||||
|
nextPage?(...args: unknown[]): unknown;
|
||||||
|
previousPage?(...args: unknown[]): unknown;
|
||||||
|
};
|
||||||
|
rowClickHandler?(...args: unknown[]): unknown;
|
||||||
|
children?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const T2Table = (tableOptions: T2TableProps): ReactElement => {
|
||||||
const {
|
const {
|
||||||
sourceData,
|
sourceData,
|
||||||
columns,
|
columns,
|
||||||
@@ -60,7 +71,7 @@ export const T2Table = (tableOptions): ReactElement => {
|
|||||||
columns,
|
columns,
|
||||||
manualPagination: true,
|
manualPagination: true,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
pageCount: sourceData.length ?? -1,
|
pageCount: sourceData?.length ?? -1,
|
||||||
state: {
|
state: {
|
||||||
pagination,
|
pagination,
|
||||||
},
|
},
|
||||||
@@ -70,42 +81,47 @@ export const T2Table = (tableOptions): ReactElement => {
|
|||||||
return (
|
return (
|
||||||
<div className="container max-w-fit mx-14">
|
<div className="container max-w-fit mx-14">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex flex-row gap-2 justify-between mt-7">
|
<div className="flex flex-row gap-2 justify-between mt-6 mb-4">
|
||||||
{/* Search bar */}
|
{/* Search bar */}
|
||||||
{tableOptions.children}
|
{tableOptions.children}
|
||||||
{/* pagination controls */}
|
|
||||||
<div>
|
{/* Pagination controls */}
|
||||||
Page {pageIndex} of {Math.ceil(totalPages / pageSize)}
|
<div className="text-sm text-gray-800 dark:text-slate-200">
|
||||||
<p>{totalPages} comics in all</p>
|
<div className="mb-1">
|
||||||
{/* Prev/Next buttons */}
|
Page {pageIndex} of {Math.ceil(totalPages / pageSize)}
|
||||||
<div className="inline-flex flex-row mt-4 mb-4">
|
</div>
|
||||||
|
<p className="text-xs text-gray-600 dark:text-slate-400">
|
||||||
|
{totalPages} comics in all
|
||||||
|
</p>
|
||||||
|
<div className="inline-flex flex-row mt-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => goToPreviousPage()}
|
onClick={() => goToPreviousPage()}
|
||||||
disabled={pageIndex === 1}
|
disabled={pageIndex === 1}
|
||||||
className="dark:bg-slate-500 bg-slate-400 rounded-l border-slate-600 border-r pt-2 px-2"
|
className="dark:bg-slate-400 bg-gray-300 rounded-l px-2 py-1 border-r border-slate-600"
|
||||||
>
|
>
|
||||||
<i className="icon-[solar--arrow-left-linear] h-6 w-6"></i>
|
<i className="icon-[solar--arrow-left-linear] h-5 w-5"></i>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="dark:bg-slate-500 bg-slate-400 rounded-r pt-2 px-2"
|
className="dark:bg-slate-400 bg-gray-300 rounded-r px-2 py-1"
|
||||||
onClick={() => goToNextPage()}
|
onClick={() => goToNextPage()}
|
||||||
disabled={pageIndex > Math.floor(totalPages / pageSize)}
|
disabled={pageIndex > Math.floor(totalPages / pageSize)}
|
||||||
>
|
>
|
||||||
<i className="icon-[solar--arrow-right-linear] h-6 w-6"></i>
|
<i className="icon-[solar--arrow-right-linear] h-5 w-5"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table className="table-auto overflow-auto">
|
|
||||||
<thead className="sticky top-0 bg-slate-200 dark:bg-slate-500">
|
<table className="table-auto w-full text-sm text-gray-900 dark:text-slate-100 border-separate border-spacing-0">
|
||||||
{table.getHeaderGroups().map((headerGroup, idx) => (
|
<thead className="sticky top-0 bg-white dark:bg-slate-900 z-10 border-b border-gray-300 dark:border-slate-700">
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
<tr key={headerGroup.id}>
|
<tr key={headerGroup.id}>
|
||||||
{headerGroup.headers.map((header, idx) => (
|
{headerGroup.headers.map((header) => (
|
||||||
<th
|
<th
|
||||||
key={header.id}
|
key={header.id}
|
||||||
colSpan={header.colSpan}
|
colSpan={header.colSpan}
|
||||||
className="px-3 py-3"
|
className="px-3 py-2 text-[11px] font-semibold tracking-wide uppercase text-left text-gray-500 dark:text-slate-400"
|
||||||
>
|
>
|
||||||
{header.isPlaceholder
|
{header.isPlaceholder
|
||||||
? null
|
? null
|
||||||
@@ -120,37 +136,23 @@ export const T2Table = (tableOptions): ReactElement => {
|
|||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
{table.getRowModel().rows.map((row, idx) => {
|
{table.getRowModel().rows.map((row) => (
|
||||||
return (
|
<tr
|
||||||
<tr key={row.id} onClick={() => rowClickHandler(row)}>
|
key={row.id}
|
||||||
{row.getVisibleCells().map((cell) => {
|
onClick={() => rowClickHandler(row)}
|
||||||
return (
|
className="border-b border-gray-200 dark:border-slate-700 hover:bg-gray-50 dark:hover:bg-slate-800 transition-colors"
|
||||||
<td key={cell.id} className="align-top">
|
>
|
||||||
{flexRender(
|
{row.getVisibleCells().map((cell) => (
|
||||||
cell.column.columnDef.cell,
|
<td key={cell.id} className="px-3 py-2 align-top">
|
||||||
cell.getContext(),
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
)}
|
</td>
|
||||||
</td>
|
))}
|
||||||
);
|
</tr>
|
||||||
})}
|
))}
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
T2Table.propTypes = {
|
|
||||||
sourceData: PropTypes.array,
|
|
||||||
totalPages: PropTypes.number,
|
|
||||||
columns: PropTypes.array,
|
|
||||||
paginationHandlers: PropTypes.shape({
|
|
||||||
nextPage: PropTypes.func,
|
|
||||||
previousPage: PropTypes.func,
|
|
||||||
}),
|
|
||||||
rowClickHandler: PropTypes.func,
|
|
||||||
children: PropTypes.any,
|
|
||||||
};
|
|
||||||
export default T2Table;
|
export default T2Table;
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
"displayName": "Dashboard"
|
"displayName": "Dashboard"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "gen-gls",
|
"id": "gen-docker-vars",
|
||||||
"displayName": "Global Search"
|
"displayName": "Docker ENV vars"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ export const determineCoverFile = (data): any => {
|
|||||||
// comicvine
|
// comicvine
|
||||||
if (!isEmpty(data.comicvine)) {
|
if (!isEmpty(data.comicvine)) {
|
||||||
coverFile.comicvine.url = data?.comicvine?.image.small_url;
|
coverFile.comicvine.url = data?.comicvine?.image.small_url;
|
||||||
coverFile.comicvine.issueName = data.comicvine.name;
|
coverFile.comicvine.issueName = data.comicvine?.name;
|
||||||
coverFile.comicvine.publisher = data.comicvine.publisher.name;
|
coverFile.comicvine.publisher = data.comicvine?.publisher?.name;
|
||||||
}
|
}
|
||||||
// rawFileDetails
|
// rawFileDetails
|
||||||
if (!isEmpty(data.rawFileDetails)) {
|
if (!isEmpty(data.rawFileDetails)) {
|
||||||
|
|||||||
@@ -1,161 +1,177 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { isNil } from "lodash";
|
import io, { Socket } from "socket.io-client";
|
||||||
import io from "socket.io-client";
|
|
||||||
import { SOCKET_BASE_URI } from "../constants/endpoints";
|
import { SOCKET_BASE_URI } from "../constants/endpoints";
|
||||||
import { produce } from "immer";
|
import { isNil } from "lodash";
|
||||||
import { QueryClient } from "@tanstack/react-query";
|
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import "react-toastify/dist/ReactToastify.min.css";
|
import "react-toastify/dist/ReactToastify.min.css";
|
||||||
|
|
||||||
/* Broadly, this file sets up:
|
// Type for global state
|
||||||
* 1. The zustand-based global client state
|
interface StoreState {
|
||||||
* 2. socket.io client
|
socketInstances: Record<string, Socket>;
|
||||||
*/
|
getSocket: (namespace?: string) => Socket;
|
||||||
export const useStore = create((set, get) => ({
|
disconnectSocket: (namespace: string) => void;
|
||||||
// Socket.io state
|
queryClientRef: { current: any } | null;
|
||||||
socketIOInstance: {},
|
setQueryClientRef: (ref: any) => void;
|
||||||
// ComicVine Scraping status
|
|
||||||
|
comicvine: {
|
||||||
|
scrapingStatus: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
importJobQueue: {
|
||||||
|
successfulJobCount: number;
|
||||||
|
failedJobCount: number;
|
||||||
|
status: string | undefined;
|
||||||
|
mostRecentImport: string | null;
|
||||||
|
|
||||||
|
setStatus: (status: string) => void;
|
||||||
|
setJobCount: (jobType: string, count: number) => void;
|
||||||
|
setMostRecentImport: (fileName: string) => void;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useStore = create<StoreState>((set, get) => ({
|
||||||
|
socketInstances: {},
|
||||||
|
queryClientRef: null,
|
||||||
|
setQueryClientRef: (ref: any) => set({ queryClientRef: ref }),
|
||||||
|
|
||||||
|
getSocket: (namespace = "/") => {
|
||||||
|
const fullNamespace = namespace === "/" ? "" : namespace;
|
||||||
|
const existing = get().socketInstances[namespace];
|
||||||
|
|
||||||
|
if (existing && existing.connected) return existing;
|
||||||
|
|
||||||
|
const sessionId = localStorage.getItem("sessionId");
|
||||||
|
const socket = io(`${SOCKET_BASE_URI}${fullNamespace}`, {
|
||||||
|
transports: ["websocket"],
|
||||||
|
withCredentials: true,
|
||||||
|
query: { sessionId },
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("connect", () => {
|
||||||
|
console.log(`✅ Connected to ${namespace}:`, socket.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (sessionId) {
|
||||||
|
socket.emit("call", "socket.resumeSession", { sessionId, namespace });
|
||||||
|
} else {
|
||||||
|
socket.on("sessionInitialized", (id) => {
|
||||||
|
localStorage.setItem("sessionId", id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.on("RESTORE_JOB_COUNTS_AFTER_SESSION_RESTORATION", (data) => {
|
||||||
|
const { completedJobCount, failedJobCount, queueStatus } = data;
|
||||||
|
set((state) => ({
|
||||||
|
importJobQueue: {
|
||||||
|
...state.importJobQueue,
|
||||||
|
successfulJobCount: completedJobCount,
|
||||||
|
failedJobCount,
|
||||||
|
status: queueStatus,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("LS_COVER_EXTRACTED", ({ completedJobCount, importResult }) => {
|
||||||
|
set((state) => ({
|
||||||
|
importJobQueue: {
|
||||||
|
...state.importJobQueue,
|
||||||
|
successfulJobCount: completedJobCount,
|
||||||
|
mostRecentImport: importResult.data.rawFileDetails.name,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("LS_COVER_EXTRACTION_FAILED", ({ failedJobCount }) => {
|
||||||
|
set((state) => ({
|
||||||
|
importJobQueue: {
|
||||||
|
...state.importJobQueue,
|
||||||
|
failedJobCount,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("LS_IMPORT_QUEUE_DRAINED", () => {
|
||||||
|
localStorage.removeItem("sessionId");
|
||||||
|
set((state) => ({
|
||||||
|
importJobQueue: {
|
||||||
|
...state.importJobQueue,
|
||||||
|
status: "drained",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
const queryClientRef = get().queryClientRef;
|
||||||
|
if (queryClientRef?.current) {
|
||||||
|
queryClientRef.current.invalidateQueries({ queryKey: ["allImportJobResults"] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("CV_SCRAPING_STATUS", (data) => {
|
||||||
|
set((state) => ({
|
||||||
|
comicvine: {
|
||||||
|
...state.comicvine,
|
||||||
|
scrapingStatus: data.message,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("searchResultsAvailable", (data) => {
|
||||||
|
toast(`Results found for query: ${JSON.stringify(data.query, null, 2)}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
set((state) => ({
|
||||||
|
socketInstances: {
|
||||||
|
...state.socketInstances,
|
||||||
|
[namespace]: socket,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return socket;
|
||||||
|
},
|
||||||
|
|
||||||
|
disconnectSocket: (namespace: string) => {
|
||||||
|
const socket = get().socketInstances[namespace];
|
||||||
|
if (socket) {
|
||||||
|
socket.disconnect();
|
||||||
|
set((state) => {
|
||||||
|
const { [namespace]: _, ...rest } = state.socketInstances;
|
||||||
|
return { socketInstances: rest };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
comicvine: {
|
comicvine: {
|
||||||
scrapingStatus: "",
|
scrapingStatus: "",
|
||||||
},
|
},
|
||||||
|
|
||||||
// Import job queue and associated statuses
|
|
||||||
importJobQueue: {
|
importJobQueue: {
|
||||||
successfulJobCount: 0,
|
successfulJobCount: 0,
|
||||||
failedJobCount: 0,
|
failedJobCount: 0,
|
||||||
status: undefined,
|
status: undefined,
|
||||||
setStatus: (status: string) =>
|
|
||||||
set(
|
|
||||||
produce((draftState) => {
|
|
||||||
draftState.importJobQueue.status = status;
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
setJobCount: (jobType: string, count: Number) => {
|
|
||||||
switch (jobType) {
|
|
||||||
case "successful":
|
|
||||||
set(
|
|
||||||
produce((draftState) => {
|
|
||||||
draftState.importJobQueue.successfulJobCount = count;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "failed":
|
|
||||||
set(
|
|
||||||
produce((draftState) => {
|
|
||||||
draftState.importJobQueue.failedJobCount = count;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mostRecentImport: null,
|
mostRecentImport: null,
|
||||||
setMostRecentImport: (fileName: string) => {
|
|
||||||
set(
|
setStatus: (status: string) =>
|
||||||
produce((state) => {
|
set((state) => ({
|
||||||
state.importJobQueue.mostRecentImport = fileName;
|
importJobQueue: {
|
||||||
}),
|
...state.importJobQueue,
|
||||||
);
|
status,
|
||||||
},
|
},
|
||||||
|
})),
|
||||||
|
|
||||||
|
setJobCount: (jobType: string, count: number) =>
|
||||||
|
set((state) => ({
|
||||||
|
importJobQueue: {
|
||||||
|
...state.importJobQueue,
|
||||||
|
...(jobType === "successful"
|
||||||
|
? { successfulJobCount: count }
|
||||||
|
: { failedJobCount: count }),
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
|
||||||
|
setMostRecentImport: (fileName: string) =>
|
||||||
|
set((state) => ({
|
||||||
|
importJobQueue: {
|
||||||
|
...state.importJobQueue,
|
||||||
|
mostRecentImport: fileName,
|
||||||
|
},
|
||||||
|
})),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { getState, setState } = useStore;
|
|
||||||
const queryClient = new QueryClient();
|
|
||||||
|
|
||||||
/** Socket.IO initialization **/
|
|
||||||
// 1. Fetch sessionId from localStorage
|
|
||||||
const sessionId = localStorage.getItem("sessionId");
|
|
||||||
// 2. socket.io instantiation
|
|
||||||
const socketIOInstance = io(SOCKET_BASE_URI, {
|
|
||||||
transports: ["websocket"],
|
|
||||||
withCredentials: true,
|
|
||||||
query: { sessionId },
|
|
||||||
});
|
|
||||||
// 3. Set the instance in global state
|
|
||||||
setState({
|
|
||||||
socketIOInstance,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Socket.io-based session restoration
|
|
||||||
if (!isNil(sessionId)) {
|
|
||||||
// 1. Resume the session
|
|
||||||
socketIOInstance.emit(
|
|
||||||
"call",
|
|
||||||
"socket.resumeSession",
|
|
||||||
{
|
|
||||||
namespace: "/",
|
|
||||||
sessionId,
|
|
||||||
},
|
|
||||||
(data) => console.log(data),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// 1. Inititalize the session and persist the sessionId to localStorage
|
|
||||||
socketIOInstance.on("sessionInitialized", (sessionId) => {
|
|
||||||
localStorage.setItem("sessionId", sessionId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// 2. If a job is in progress, restore the job counts and persist those to global state
|
|
||||||
socketIOInstance.on("RESTORE_JOB_COUNTS_AFTER_SESSION_RESTORATION", (data) => {
|
|
||||||
console.log("Active import in progress detected; restoring counts...");
|
|
||||||
const { completedJobCount, failedJobCount, queueStatus } = data;
|
|
||||||
setState((state) => ({
|
|
||||||
importJobQueue: {
|
|
||||||
...state.importJobQueue,
|
|
||||||
successfulJobCount: completedJobCount,
|
|
||||||
failedJobCount,
|
|
||||||
status: queueStatus,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
// 1a. Act on each comic issue successfully imported/failed, as indicated
|
|
||||||
// by the LS_COVER_EXTRACTED/LS_COVER_EXTRACTION_FAILED events
|
|
||||||
socketIOInstance.on("LS_COVER_EXTRACTED", (data) => {
|
|
||||||
const { completedJobCount, importResult } = data;
|
|
||||||
console.log(importResult);
|
|
||||||
setState((state) => ({
|
|
||||||
importJobQueue: {
|
|
||||||
...state.importJobQueue,
|
|
||||||
successfulJobCount: completedJobCount,
|
|
||||||
mostRecentImport: importResult.data.rawFileDetails.name,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
socketIOInstance.on("LS_COVER_EXTRACTION_FAILED", (data) => {
|
|
||||||
const { failedJobCount } = data;
|
|
||||||
setState((state) => ({
|
|
||||||
importJobQueue: {
|
|
||||||
...state.importJobQueue,
|
|
||||||
failedJobCount,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
socketIOInstance.on("searchResultsAvailable", (data) => {
|
|
||||||
console.log(data);
|
|
||||||
toast(`Results found for query: ${JSON.stringify(data.query, null, 2)}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 1b. Clear the localStorage sessionId upon receiving the
|
|
||||||
// LS_IMPORT_QUEUE_DRAINED event
|
|
||||||
socketIOInstance.on("LS_IMPORT_QUEUE_DRAINED", (data) => {
|
|
||||||
localStorage.removeItem("sessionId");
|
|
||||||
setState((state) => ({
|
|
||||||
importJobQueue: {
|
|
||||||
...state.importJobQueue,
|
|
||||||
status: "drained",
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["allImportJobResults"] });
|
|
||||||
});
|
|
||||||
|
|
||||||
// ComicVine Scraping status
|
|
||||||
socketIOInstance.on("CV_SCRAPING_STATUS", (data) => {
|
|
||||||
setState((state) => ({
|
|
||||||
comicvine: {
|
|
||||||
...state.comicvine,
|
|
||||||
scrapingStatus: data.message,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|||||||
634
yarn.lock
634
yarn.lock
@@ -35,6 +35,15 @@
|
|||||||
"@babel/highlight" "^7.23.4"
|
"@babel/highlight" "^7.23.4"
|
||||||
chalk "^2.4.2"
|
chalk "^2.4.2"
|
||||||
|
|
||||||
|
"@babel/code-frame@^7.27.1":
|
||||||
|
version "7.27.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be"
|
||||||
|
integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==
|
||||||
|
dependencies:
|
||||||
|
"@babel/helper-validator-identifier" "^7.27.1"
|
||||||
|
js-tokens "^4.0.0"
|
||||||
|
picocolors "^1.1.1"
|
||||||
|
|
||||||
"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.23.3", "@babel/compat-data@^7.23.5":
|
"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.23.3", "@babel/compat-data@^7.23.5":
|
||||||
version "7.23.5"
|
version "7.23.5"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.5.tgz#ffb878728bb6bdcb6f4510aa51b1be9afb8cfd98"
|
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.5.tgz#ffb878728bb6bdcb6f4510aa51b1be9afb8cfd98"
|
||||||
@@ -232,11 +241,21 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz#9478c707febcbbe1ddb38a3d91a2e054ae622d83"
|
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz#9478c707febcbbe1ddb38a3d91a2e054ae622d83"
|
||||||
integrity sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==
|
integrity sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==
|
||||||
|
|
||||||
|
"@babel/helper-string-parser@^7.27.1":
|
||||||
|
version "7.27.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687"
|
||||||
|
integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==
|
||||||
|
|
||||||
"@babel/helper-validator-identifier@^7.22.20":
|
"@babel/helper-validator-identifier@^7.22.20":
|
||||||
version "7.22.20"
|
version "7.22.20"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0"
|
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0"
|
||||||
integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==
|
integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==
|
||||||
|
|
||||||
|
"@babel/helper-validator-identifier@^7.27.1":
|
||||||
|
version "7.27.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8"
|
||||||
|
integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==
|
||||||
|
|
||||||
"@babel/helper-validator-option@^7.22.15", "@babel/helper-validator-option@^7.23.5":
|
"@babel/helper-validator-option@^7.22.15", "@babel/helper-validator-option@^7.23.5":
|
||||||
version "7.23.5"
|
version "7.23.5"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz#907a3fbd4523426285365d1206c423c4c5520307"
|
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz#907a3fbd4523426285365d1206c423c4c5520307"
|
||||||
@@ -252,13 +271,12 @@
|
|||||||
"@babel/types" "^7.22.19"
|
"@babel/types" "^7.22.19"
|
||||||
|
|
||||||
"@babel/helpers@^7.23.9":
|
"@babel/helpers@^7.23.9":
|
||||||
version "7.23.9"
|
version "7.27.4"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.9.tgz#c3e20bbe7f7a7e10cb9b178384b4affdf5995c7d"
|
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.27.4.tgz#c79050c6a0e41e095bfc96d469c85431e9ed7fe7"
|
||||||
integrity sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==
|
integrity sha512-Y+bO6U+I7ZKaM5G5rDUZiYfUvQPUibYmAFe7EnKdnKBbVXDZxvp+MWOH5gYciY0EPk4EScsuFMQBbEfpdRKSCQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/template" "^7.23.9"
|
"@babel/template" "^7.27.2"
|
||||||
"@babel/traverse" "^7.23.9"
|
"@babel/types" "^7.27.3"
|
||||||
"@babel/types" "^7.23.9"
|
|
||||||
|
|
||||||
"@babel/highlight@^7.23.4":
|
"@babel/highlight@^7.23.4":
|
||||||
version "7.23.4"
|
version "7.23.4"
|
||||||
@@ -274,6 +292,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.9.tgz#7b903b6149b0f8fa7ad564af646c4c38a77fc44b"
|
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.9.tgz#7b903b6149b0f8fa7ad564af646c4c38a77fc44b"
|
||||||
integrity sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==
|
integrity sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==
|
||||||
|
|
||||||
|
"@babel/parser@^7.27.2":
|
||||||
|
version "7.27.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.27.5.tgz#ed22f871f110aa285a6fd934a0efed621d118826"
|
||||||
|
integrity sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==
|
||||||
|
dependencies:
|
||||||
|
"@babel/types" "^7.27.3"
|
||||||
|
|
||||||
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.23.3":
|
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.23.3":
|
||||||
version "7.23.3"
|
version "7.23.3"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.23.3.tgz#5cd1c87ba9380d0afb78469292c954fee5d2411a"
|
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.23.3.tgz#5cd1c87ba9380d0afb78469292c954fee5d2411a"
|
||||||
@@ -1040,6 +1065,15 @@
|
|||||||
"@babel/parser" "^7.23.9"
|
"@babel/parser" "^7.23.9"
|
||||||
"@babel/types" "^7.23.9"
|
"@babel/types" "^7.23.9"
|
||||||
|
|
||||||
|
"@babel/template@^7.27.2":
|
||||||
|
version "7.27.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d"
|
||||||
|
integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/code-frame" "^7.27.1"
|
||||||
|
"@babel/parser" "^7.27.2"
|
||||||
|
"@babel/types" "^7.27.1"
|
||||||
|
|
||||||
"@babel/traverse@^7.18.9", "@babel/traverse@^7.23.2", "@babel/traverse@^7.23.9":
|
"@babel/traverse@^7.18.9", "@babel/traverse@^7.23.2", "@babel/traverse@^7.23.9":
|
||||||
version "7.23.9"
|
version "7.23.9"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.9.tgz#2f9d6aead6b564669394c5ce0f9302bb65b9d950"
|
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.9.tgz#2f9d6aead6b564669394c5ce0f9302bb65b9d950"
|
||||||
@@ -1065,6 +1099,14 @@
|
|||||||
"@babel/helper-validator-identifier" "^7.22.20"
|
"@babel/helper-validator-identifier" "^7.22.20"
|
||||||
to-fast-properties "^2.0.0"
|
to-fast-properties "^2.0.0"
|
||||||
|
|
||||||
|
"@babel/types@^7.27.1", "@babel/types@^7.27.3":
|
||||||
|
version "7.27.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.27.3.tgz#c0257bedf33aad6aad1f406d35c44758321eb3ec"
|
||||||
|
integrity sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/helper-string-parser" "^7.27.1"
|
||||||
|
"@babel/helper-validator-identifier" "^7.27.1"
|
||||||
|
|
||||||
"@base2/pretty-print-object@1.0.1":
|
"@base2/pretty-print-object@1.0.1":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@base2/pretty-print-object/-/pretty-print-object-1.0.1.tgz#371ba8be66d556812dc7fb169ebc3c08378f69d4"
|
resolved "https://registry.yarnpkg.com/@base2/pretty-print-object/-/pretty-print-object-1.0.1.tgz#371ba8be66d556812dc7fb169ebc3c08378f69d4"
|
||||||
@@ -1254,230 +1296,230 @@
|
|||||||
esquery "^1.5.0"
|
esquery "^1.5.0"
|
||||||
jsdoc-type-pratt-parser "~4.0.0"
|
jsdoc-type-pratt-parser "~4.0.0"
|
||||||
|
|
||||||
"@esbuild/aix-ppc64@0.20.2":
|
"@esbuild/aix-ppc64@0.21.5":
|
||||||
version "0.20.2"
|
version "0.21.5"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537"
|
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f"
|
||||||
integrity sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==
|
integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==
|
||||||
|
|
||||||
"@esbuild/android-arm64@0.18.20":
|
"@esbuild/android-arm64@0.18.20":
|
||||||
version "0.18.20"
|
version "0.18.20"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz#984b4f9c8d0377443cc2dfcef266d02244593622"
|
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz#984b4f9c8d0377443cc2dfcef266d02244593622"
|
||||||
integrity sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==
|
integrity sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==
|
||||||
|
|
||||||
"@esbuild/android-arm64@0.20.2":
|
"@esbuild/android-arm64@0.21.5":
|
||||||
version "0.20.2"
|
version "0.21.5"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz#db1c9202a5bc92ea04c7b6840f1bbe09ebf9e6b9"
|
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052"
|
||||||
integrity sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==
|
integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==
|
||||||
|
|
||||||
"@esbuild/android-arm@0.18.20":
|
"@esbuild/android-arm@0.18.20":
|
||||||
version "0.18.20"
|
version "0.18.20"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz#fedb265bc3a589c84cc11f810804f234947c3682"
|
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz#fedb265bc3a589c84cc11f810804f234947c3682"
|
||||||
integrity sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==
|
integrity sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==
|
||||||
|
|
||||||
"@esbuild/android-arm@0.20.2":
|
"@esbuild/android-arm@0.21.5":
|
||||||
version "0.20.2"
|
version "0.21.5"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.2.tgz#3b488c49aee9d491c2c8f98a909b785870d6e995"
|
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28"
|
||||||
integrity sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==
|
integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==
|
||||||
|
|
||||||
"@esbuild/android-x64@0.18.20":
|
"@esbuild/android-x64@0.18.20":
|
||||||
version "0.18.20"
|
version "0.18.20"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz#35cf419c4cfc8babe8893d296cd990e9e9f756f2"
|
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz#35cf419c4cfc8babe8893d296cd990e9e9f756f2"
|
||||||
integrity sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==
|
integrity sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==
|
||||||
|
|
||||||
"@esbuild/android-x64@0.20.2":
|
"@esbuild/android-x64@0.21.5":
|
||||||
version "0.20.2"
|
version "0.21.5"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.2.tgz#3b1628029e5576249d2b2d766696e50768449f98"
|
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e"
|
||||||
integrity sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==
|
integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==
|
||||||
|
|
||||||
"@esbuild/darwin-arm64@0.18.20":
|
"@esbuild/darwin-arm64@0.18.20":
|
||||||
version "0.18.20"
|
version "0.18.20"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz#08172cbeccf95fbc383399a7f39cfbddaeb0d7c1"
|
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz#08172cbeccf95fbc383399a7f39cfbddaeb0d7c1"
|
||||||
integrity sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==
|
integrity sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==
|
||||||
|
|
||||||
"@esbuild/darwin-arm64@0.20.2":
|
"@esbuild/darwin-arm64@0.21.5":
|
||||||
version "0.20.2"
|
version "0.21.5"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz#6e8517a045ddd86ae30c6608c8475ebc0c4000bb"
|
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a"
|
||||||
integrity sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==
|
integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==
|
||||||
|
|
||||||
"@esbuild/darwin-x64@0.18.20":
|
"@esbuild/darwin-x64@0.18.20":
|
||||||
version "0.18.20"
|
version "0.18.20"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz#d70d5790d8bf475556b67d0f8b7c5bdff053d85d"
|
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz#d70d5790d8bf475556b67d0f8b7c5bdff053d85d"
|
||||||
integrity sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==
|
integrity sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==
|
||||||
|
|
||||||
"@esbuild/darwin-x64@0.20.2":
|
"@esbuild/darwin-x64@0.21.5":
|
||||||
version "0.20.2"
|
version "0.21.5"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz#90ed098e1f9dd8a9381695b207e1cff45540a0d0"
|
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22"
|
||||||
integrity sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==
|
integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==
|
||||||
|
|
||||||
"@esbuild/freebsd-arm64@0.18.20":
|
"@esbuild/freebsd-arm64@0.18.20":
|
||||||
version "0.18.20"
|
version "0.18.20"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz#98755cd12707f93f210e2494d6a4b51b96977f54"
|
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz#98755cd12707f93f210e2494d6a4b51b96977f54"
|
||||||
integrity sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==
|
integrity sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==
|
||||||
|
|
||||||
"@esbuild/freebsd-arm64@0.20.2":
|
"@esbuild/freebsd-arm64@0.21.5":
|
||||||
version "0.20.2"
|
version "0.21.5"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz#d71502d1ee89a1130327e890364666c760a2a911"
|
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e"
|
||||||
integrity sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==
|
integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==
|
||||||
|
|
||||||
"@esbuild/freebsd-x64@0.18.20":
|
"@esbuild/freebsd-x64@0.18.20":
|
||||||
version "0.18.20"
|
version "0.18.20"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz#c1eb2bff03915f87c29cece4c1a7fa1f423b066e"
|
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz#c1eb2bff03915f87c29cece4c1a7fa1f423b066e"
|
||||||
integrity sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==
|
integrity sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==
|
||||||
|
|
||||||
"@esbuild/freebsd-x64@0.20.2":
|
"@esbuild/freebsd-x64@0.21.5":
|
||||||
version "0.20.2"
|
version "0.21.5"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz#aa5ea58d9c1dd9af688b8b6f63ef0d3d60cea53c"
|
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261"
|
||||||
integrity sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==
|
integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==
|
||||||
|
|
||||||
"@esbuild/linux-arm64@0.18.20":
|
"@esbuild/linux-arm64@0.18.20":
|
||||||
version "0.18.20"
|
version "0.18.20"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz#bad4238bd8f4fc25b5a021280c770ab5fc3a02a0"
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz#bad4238bd8f4fc25b5a021280c770ab5fc3a02a0"
|
||||||
integrity sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==
|
integrity sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==
|
||||||
|
|
||||||
"@esbuild/linux-arm64@0.20.2":
|
"@esbuild/linux-arm64@0.21.5":
|
||||||
version "0.20.2"
|
version "0.21.5"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz#055b63725df678379b0f6db9d0fa85463755b2e5"
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b"
|
||||||
integrity sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==
|
integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==
|
||||||
|
|
||||||
"@esbuild/linux-arm@0.18.20":
|
"@esbuild/linux-arm@0.18.20":
|
||||||
version "0.18.20"
|
version "0.18.20"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz#3e617c61f33508a27150ee417543c8ab5acc73b0"
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz#3e617c61f33508a27150ee417543c8ab5acc73b0"
|
||||||
integrity sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==
|
integrity sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==
|
||||||
|
|
||||||
"@esbuild/linux-arm@0.20.2":
|
"@esbuild/linux-arm@0.21.5":
|
||||||
version "0.20.2"
|
version "0.21.5"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz#76b3b98cb1f87936fbc37f073efabad49dcd889c"
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9"
|
||||||
integrity sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==
|
integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==
|
||||||
|
|
||||||
"@esbuild/linux-ia32@0.18.20":
|
"@esbuild/linux-ia32@0.18.20":
|
||||||
version "0.18.20"
|
version "0.18.20"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz#699391cccba9aee6019b7f9892eb99219f1570a7"
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz#699391cccba9aee6019b7f9892eb99219f1570a7"
|
||||||
integrity sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==
|
integrity sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==
|
||||||
|
|
||||||
"@esbuild/linux-ia32@0.20.2":
|
"@esbuild/linux-ia32@0.21.5":
|
||||||
version "0.20.2"
|
version "0.21.5"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz#c0e5e787c285264e5dfc7a79f04b8b4eefdad7fa"
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2"
|
||||||
integrity sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==
|
integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==
|
||||||
|
|
||||||
"@esbuild/linux-loong64@0.18.20":
|
"@esbuild/linux-loong64@0.18.20":
|
||||||
version "0.18.20"
|
version "0.18.20"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz#e6fccb7aac178dd2ffb9860465ac89d7f23b977d"
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz#e6fccb7aac178dd2ffb9860465ac89d7f23b977d"
|
||||||
integrity sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==
|
integrity sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==
|
||||||
|
|
||||||
"@esbuild/linux-loong64@0.20.2":
|
"@esbuild/linux-loong64@0.21.5":
|
||||||
version "0.20.2"
|
version "0.21.5"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz#a6184e62bd7cdc63e0c0448b83801001653219c5"
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df"
|
||||||
integrity sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==
|
integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==
|
||||||
|
|
||||||
"@esbuild/linux-mips64el@0.18.20":
|
"@esbuild/linux-mips64el@0.18.20":
|
||||||
version "0.18.20"
|
version "0.18.20"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz#eeff3a937de9c2310de30622a957ad1bd9183231"
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz#eeff3a937de9c2310de30622a957ad1bd9183231"
|
||||||
integrity sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==
|
integrity sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==
|
||||||
|
|
||||||
"@esbuild/linux-mips64el@0.20.2":
|
"@esbuild/linux-mips64el@0.21.5":
|
||||||
version "0.20.2"
|
version "0.21.5"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz#d08e39ce86f45ef8fc88549d29c62b8acf5649aa"
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe"
|
||||||
integrity sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==
|
integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==
|
||||||
|
|
||||||
"@esbuild/linux-ppc64@0.18.20":
|
"@esbuild/linux-ppc64@0.18.20":
|
||||||
version "0.18.20"
|
version "0.18.20"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz#2f7156bde20b01527993e6881435ad79ba9599fb"
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz#2f7156bde20b01527993e6881435ad79ba9599fb"
|
||||||
integrity sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==
|
integrity sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==
|
||||||
|
|
||||||
"@esbuild/linux-ppc64@0.20.2":
|
"@esbuild/linux-ppc64@0.21.5":
|
||||||
version "0.20.2"
|
version "0.21.5"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz#8d252f0b7756ffd6d1cbde5ea67ff8fd20437f20"
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4"
|
||||||
integrity sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==
|
integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==
|
||||||
|
|
||||||
"@esbuild/linux-riscv64@0.18.20":
|
"@esbuild/linux-riscv64@0.18.20":
|
||||||
version "0.18.20"
|
version "0.18.20"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz#6628389f210123d8b4743045af8caa7d4ddfc7a6"
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz#6628389f210123d8b4743045af8caa7d4ddfc7a6"
|
||||||
integrity sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==
|
integrity sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==
|
||||||
|
|
||||||
"@esbuild/linux-riscv64@0.20.2":
|
"@esbuild/linux-riscv64@0.21.5":
|
||||||
version "0.20.2"
|
version "0.21.5"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz#19f6dcdb14409dae607f66ca1181dd4e9db81300"
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc"
|
||||||
integrity sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==
|
integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==
|
||||||
|
|
||||||
"@esbuild/linux-s390x@0.18.20":
|
"@esbuild/linux-s390x@0.18.20":
|
||||||
version "0.18.20"
|
version "0.18.20"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz#255e81fb289b101026131858ab99fba63dcf0071"
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz#255e81fb289b101026131858ab99fba63dcf0071"
|
||||||
integrity sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==
|
integrity sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==
|
||||||
|
|
||||||
"@esbuild/linux-s390x@0.20.2":
|
"@esbuild/linux-s390x@0.21.5":
|
||||||
version "0.20.2"
|
version "0.21.5"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz#3c830c90f1a5d7dd1473d5595ea4ebb920988685"
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de"
|
||||||
integrity sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==
|
integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==
|
||||||
|
|
||||||
"@esbuild/linux-x64@0.18.20":
|
"@esbuild/linux-x64@0.18.20":
|
||||||
version "0.18.20"
|
version "0.18.20"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz#c7690b3417af318a9b6f96df3031a8865176d338"
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz#c7690b3417af318a9b6f96df3031a8865176d338"
|
||||||
integrity sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==
|
integrity sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==
|
||||||
|
|
||||||
"@esbuild/linux-x64@0.20.2":
|
"@esbuild/linux-x64@0.21.5":
|
||||||
version "0.20.2"
|
version "0.21.5"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz#86eca35203afc0d9de0694c64ec0ab0a378f6fff"
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0"
|
||||||
integrity sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==
|
integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==
|
||||||
|
|
||||||
"@esbuild/netbsd-x64@0.18.20":
|
"@esbuild/netbsd-x64@0.18.20":
|
||||||
version "0.18.20"
|
version "0.18.20"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz#30e8cd8a3dded63975e2df2438ca109601ebe0d1"
|
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz#30e8cd8a3dded63975e2df2438ca109601ebe0d1"
|
||||||
integrity sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==
|
integrity sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==
|
||||||
|
|
||||||
"@esbuild/netbsd-x64@0.20.2":
|
"@esbuild/netbsd-x64@0.21.5":
|
||||||
version "0.20.2"
|
version "0.21.5"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz#e771c8eb0e0f6e1877ffd4220036b98aed5915e6"
|
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047"
|
||||||
integrity sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==
|
integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==
|
||||||
|
|
||||||
"@esbuild/openbsd-x64@0.18.20":
|
"@esbuild/openbsd-x64@0.18.20":
|
||||||
version "0.18.20"
|
version "0.18.20"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz#7812af31b205055874c8082ea9cf9ab0da6217ae"
|
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz#7812af31b205055874c8082ea9cf9ab0da6217ae"
|
||||||
integrity sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==
|
integrity sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==
|
||||||
|
|
||||||
"@esbuild/openbsd-x64@0.20.2":
|
"@esbuild/openbsd-x64@0.21.5":
|
||||||
version "0.20.2"
|
version "0.21.5"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz#9a795ae4b4e37e674f0f4d716f3e226dd7c39baf"
|
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70"
|
||||||
integrity sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==
|
integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==
|
||||||
|
|
||||||
"@esbuild/sunos-x64@0.18.20":
|
"@esbuild/sunos-x64@0.18.20":
|
||||||
version "0.18.20"
|
version "0.18.20"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz#d5c275c3b4e73c9b0ecd38d1ca62c020f887ab9d"
|
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz#d5c275c3b4e73c9b0ecd38d1ca62c020f887ab9d"
|
||||||
integrity sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==
|
integrity sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==
|
||||||
|
|
||||||
"@esbuild/sunos-x64@0.20.2":
|
"@esbuild/sunos-x64@0.21.5":
|
||||||
version "0.20.2"
|
version "0.21.5"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz#7df23b61a497b8ac189def6e25a95673caedb03f"
|
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b"
|
||||||
integrity sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==
|
integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==
|
||||||
|
|
||||||
"@esbuild/win32-arm64@0.18.20":
|
"@esbuild/win32-arm64@0.18.20":
|
||||||
version "0.18.20"
|
version "0.18.20"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz#73bc7f5a9f8a77805f357fab97f290d0e4820ac9"
|
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz#73bc7f5a9f8a77805f357fab97f290d0e4820ac9"
|
||||||
integrity sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==
|
integrity sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==
|
||||||
|
|
||||||
"@esbuild/win32-arm64@0.20.2":
|
"@esbuild/win32-arm64@0.21.5":
|
||||||
version "0.20.2"
|
version "0.21.5"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz#f1ae5abf9ca052ae11c1bc806fb4c0f519bacf90"
|
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d"
|
||||||
integrity sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==
|
integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==
|
||||||
|
|
||||||
"@esbuild/win32-ia32@0.18.20":
|
"@esbuild/win32-ia32@0.18.20":
|
||||||
version "0.18.20"
|
version "0.18.20"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz#ec93cbf0ef1085cc12e71e0d661d20569ff42102"
|
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz#ec93cbf0ef1085cc12e71e0d661d20569ff42102"
|
||||||
integrity sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==
|
integrity sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==
|
||||||
|
|
||||||
"@esbuild/win32-ia32@0.20.2":
|
"@esbuild/win32-ia32@0.21.5":
|
||||||
version "0.20.2"
|
version "0.21.5"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz#241fe62c34d8e8461cd708277813e1d0ba55ce23"
|
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b"
|
||||||
integrity sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==
|
integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==
|
||||||
|
|
||||||
"@esbuild/win32-x64@0.18.20":
|
"@esbuild/win32-x64@0.18.20":
|
||||||
version "0.18.20"
|
version "0.18.20"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d"
|
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d"
|
||||||
integrity sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==
|
integrity sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==
|
||||||
|
|
||||||
"@esbuild/win32-x64@0.20.2":
|
"@esbuild/win32-x64@0.21.5":
|
||||||
version "0.20.2"
|
version "0.21.5"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc"
|
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c"
|
||||||
integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==
|
integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==
|
||||||
|
|
||||||
"@eslint-community/eslint-utils@^4.2.0":
|
"@eslint-community/eslint-utils@^4.2.0":
|
||||||
version "4.4.0"
|
version "4.4.0"
|
||||||
@@ -1572,6 +1614,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.5.1.tgz#55cc8410abf1003b726324661ce5b0d1c10de258"
|
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.5.1.tgz#55cc8410abf1003b726324661ce5b0d1c10de258"
|
||||||
integrity sha512-CNy5vSwN3fsUStPRLX7fUYojyuzoEMSXPl7zSLJ8TgtRfjv24LOnOWKT2zYwaHZCJGkdyRnTmstR0P+Ah503Gw==
|
integrity sha512-CNy5vSwN3fsUStPRLX7fUYojyuzoEMSXPl7zSLJ8TgtRfjv24LOnOWKT2zYwaHZCJGkdyRnTmstR0P+Ah503Gw==
|
||||||
|
|
||||||
|
"@graphql-typed-document-node/core@^3.2.0":
|
||||||
|
version "3.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861"
|
||||||
|
integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==
|
||||||
|
|
||||||
"@humanwhocodes/config-array@^0.11.13":
|
"@humanwhocodes/config-array@^0.11.13":
|
||||||
version "0.11.14"
|
version "0.11.14"
|
||||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b"
|
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b"
|
||||||
@@ -2307,80 +2354,100 @@
|
|||||||
estree-walker "^2.0.2"
|
estree-walker "^2.0.2"
|
||||||
picomatch "^2.3.1"
|
picomatch "^2.3.1"
|
||||||
|
|
||||||
"@rollup/rollup-android-arm-eabi@4.13.2":
|
"@rollup/rollup-android-arm-eabi@4.34.8":
|
||||||
version "4.13.2"
|
version "4.34.8"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.2.tgz#fbf098f49d96a8cac9056f22f5fd80906ef3af85"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz#731df27dfdb77189547bcef96ada7bf166bbb2fb"
|
||||||
integrity sha512-3XFIDKWMFZrMnao1mJhnOT1h2g0169Os848NhhmGweEcfJ4rCi+3yMCOLG4zA61rbJdkcrM/DjVZm9Hg5p5w7g==
|
integrity sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==
|
||||||
|
|
||||||
"@rollup/rollup-android-arm64@4.13.2":
|
"@rollup/rollup-android-arm64@4.34.8":
|
||||||
version "4.13.2"
|
version "4.34.8"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.2.tgz#0d2448251040fce19a98eee505dff5b3c8ec9b98"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.8.tgz#4bea6db78e1f6927405df7fe0faf2f5095e01343"
|
||||||
integrity sha512-GdxxXbAuM7Y/YQM9/TwwP+L0omeE/lJAR1J+olu36c3LqqZEBdsIWeQ91KBe6nxwOnb06Xh7JS2U5ooWU5/LgQ==
|
integrity sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q==
|
||||||
|
|
||||||
"@rollup/rollup-darwin-arm64@4.13.2":
|
"@rollup/rollup-darwin-arm64@4.34.8":
|
||||||
version "4.13.2"
|
version "4.34.8"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.2.tgz#78db4d4da5b1b84c22adbe25c8a4961b3f22d3af"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.8.tgz#a7aab77d44be3c44a20f946e10160f84e5450e7f"
|
||||||
integrity sha512-mCMlpzlBgOTdaFs83I4XRr8wNPveJiJX1RLfv4hggyIVhfB5mJfN4P8Z6yKh+oE4Luz+qq1P3kVdWrCKcMYrrA==
|
integrity sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q==
|
||||||
|
|
||||||
"@rollup/rollup-darwin-x64@4.13.2":
|
"@rollup/rollup-darwin-x64@4.34.8":
|
||||||
version "4.13.2"
|
version "4.34.8"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.2.tgz#fcc05af54379f8ee5c7e954987d4514c6fd0fb42"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.8.tgz#c572c024b57ee8ddd1b0851703ace9eb6cc0dd82"
|
||||||
integrity sha512-yUoEvnH0FBef/NbB1u6d3HNGyruAKnN74LrPAfDQL3O32e3k3OSfLrPgSJmgb3PJrBZWfPyt6m4ZhAFa2nZp2A==
|
integrity sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw==
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm-gnueabihf@4.13.2":
|
"@rollup/rollup-freebsd-arm64@4.34.8":
|
||||||
version "4.13.2"
|
version "4.34.8"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.2.tgz#2ce200efa1ef4a56ee2af7b453edc74a259d7d31"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.8.tgz#cf74f8113b5a83098a5c026c165742277cbfb88b"
|
||||||
integrity sha512-GYbLs5ErswU/Xs7aGXqzc3RrdEjKdmoCrgzhJWyFL0r5fL3qd1NPcDKDowDnmcoSiGJeU68/Vy+OMUluRxPiLQ==
|
integrity sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA==
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm64-gnu@4.13.2":
|
"@rollup/rollup-freebsd-x64@4.34.8":
|
||||||
version "4.13.2"
|
version "4.34.8"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.2.tgz#5a24aac882bff9abfda3f45f6f1db2166c342a4a"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.8.tgz#39561f3a2f201a4ad6a01425b1ff5928154ecd7c"
|
||||||
integrity sha512-L1+D8/wqGnKQIlh4Zre9i4R4b4noxzH5DDciyahX4oOz62CphY7WDWqJoQ66zNR4oScLNOqQJfNSIAe/6TPUmQ==
|
integrity sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q==
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm64-musl@4.13.2":
|
"@rollup/rollup-linux-arm-gnueabihf@4.34.8":
|
||||||
version "4.13.2"
|
version "4.34.8"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.2.tgz#f1fb4c6f961d3f3397231a99e621d199200e4ea9"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.8.tgz#980d6061e373bfdaeb67925c46d2f8f9b3de537f"
|
||||||
integrity sha512-tK5eoKFkXdz6vjfkSTCupUzCo40xueTOiOO6PeEIadlNBkadH1wNOH8ILCPIl8by/Gmb5AGAeQOFeLev7iZDOA==
|
integrity sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g==
|
||||||
|
|
||||||
"@rollup/rollup-linux-powerpc64le-gnu@4.13.2":
|
"@rollup/rollup-linux-arm-musleabihf@4.34.8":
|
||||||
version "4.13.2"
|
version "4.34.8"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.13.2.tgz#46b2463d94ac3af3e0f7a2947b695397bc13b755"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.8.tgz#f91a90f30dc00d5a64ac2d9bbedc829cd3cfaa78"
|
||||||
integrity sha512-zvXvAUGGEYi6tYhcDmb9wlOckVbuD+7z3mzInCSTACJ4DQrdSLPNUeDIcAQW39M3q6PDquqLWu7pnO39uSMRzQ==
|
integrity sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA==
|
||||||
|
|
||||||
"@rollup/rollup-linux-riscv64-gnu@4.13.2":
|
"@rollup/rollup-linux-arm64-gnu@4.34.8":
|
||||||
version "4.13.2"
|
version "4.34.8"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.2.tgz#47b932ee59a5395a3a341b0493e361d9e6032cf2"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.8.tgz#fac700fa5c38bc13a0d5d34463133093da4c92a0"
|
||||||
integrity sha512-C3GSKvMtdudHCN5HdmAMSRYR2kkhgdOfye4w0xzyii7lebVr4riCgmM6lRiSCnJn2w1Xz7ZZzHKuLrjx5620kw==
|
integrity sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==
|
||||||
|
|
||||||
"@rollup/rollup-linux-s390x-gnu@4.13.2":
|
"@rollup/rollup-linux-arm64-musl@4.34.8":
|
||||||
version "4.13.2"
|
version "4.34.8"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.13.2.tgz#8e14a1b3c3b9a4440c70a9c1ba12d32aa21f9712"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.8.tgz#f50ecccf8c78841ff6df1706bc4782d7f62bf9c3"
|
||||||
integrity sha512-l4U0KDFwzD36j7HdfJ5/TveEQ1fUTjFFQP5qIt9gBqBgu1G8/kCaq5Ok05kd5TG9F8Lltf3MoYsUMw3rNlJ0Yg==
|
integrity sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q==
|
||||||
|
|
||||||
"@rollup/rollup-linux-x64-gnu@4.13.2":
|
"@rollup/rollup-linux-loongarch64-gnu@4.34.8":
|
||||||
version "4.13.2"
|
version "4.34.8"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.2.tgz#270e939194b66df77bcb33dd9a5ddf7784bd7997"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.8.tgz#5869dc0b28242da6553e2b52af41374f4038cd6e"
|
||||||
integrity sha512-xXMLUAMzrtsvh3cZ448vbXqlUa7ZL8z0MwHp63K2IIID2+DeP5iWIT6g1SN7hg1VxPzqx0xZdiDM9l4n9LRU1A==
|
integrity sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ==
|
||||||
|
|
||||||
"@rollup/rollup-linux-x64-musl@4.13.2":
|
"@rollup/rollup-linux-powerpc64le-gnu@4.34.8":
|
||||||
version "4.13.2"
|
version "4.34.8"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.2.tgz#e8dd0f3c2046acbda2934490b36552e856a3bc6a"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.8.tgz#5cdd9f851ce1bea33d6844a69f9574de335f20b1"
|
||||||
integrity sha512-M/JYAWickafUijWPai4ehrjzVPKRCyDb1SLuO+ZyPfoXgeCEAlgPkNXewFZx0zcnoIe3ay4UjXIMdXQXOZXWqA==
|
integrity sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw==
|
||||||
|
|
||||||
"@rollup/rollup-win32-arm64-msvc@4.13.2":
|
"@rollup/rollup-linux-riscv64-gnu@4.34.8":
|
||||||
version "4.13.2"
|
version "4.34.8"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.2.tgz#f8b65a4a7e7a6b383e7b14439129b2f474ff123c"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.8.tgz#ef5dc37f4388f5253f0def43e1440ec012af204d"
|
||||||
integrity sha512-2YWwoVg9KRkIKaXSh0mz3NmfurpmYoBBTAXA9qt7VXk0Xy12PoOP40EFuau+ajgALbbhi4uTj3tSG3tVseCjuA==
|
integrity sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw==
|
||||||
|
|
||||||
"@rollup/rollup-win32-ia32-msvc@4.13.2":
|
"@rollup/rollup-linux-s390x-gnu@4.34.8":
|
||||||
version "4.13.2"
|
version "4.34.8"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.2.tgz#bc1c5a4fbc4337d6cb15da80a4de95fd53ab3573"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.8.tgz#7dbc3ccbcbcfb3e65be74538dfb6e8dd16178fde"
|
||||||
integrity sha512-2FSsE9aQ6OWD20E498NYKEQLneShWes0NGMPQwxWOdws35qQXH+FplabOSP5zEe1pVjurSDOGEVCE2agFwSEsw==
|
integrity sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA==
|
||||||
|
|
||||||
"@rollup/rollup-win32-x64-msvc@4.13.2":
|
"@rollup/rollup-linux-x64-gnu@4.34.8":
|
||||||
version "4.13.2"
|
version "4.34.8"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.2.tgz#851959c4c1c3c6647aba1f388198c8243aed6917"
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.8.tgz#5783fc0adcab7dc069692056e8ca8d83709855ce"
|
||||||
integrity sha512-7h7J2nokcdPePdKykd8wtc8QqqkqxIrUz7MHj6aNr8waBRU//NLDVnNjQnqQO6fqtjrtCdftpbTuOKAyrAQETQ==
|
integrity sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-musl@4.34.8":
|
||||||
|
version "4.34.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.8.tgz#00b6c29b298197a384e3c659910b47943003a678"
|
||||||
|
integrity sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ==
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-arm64-msvc@4.34.8":
|
||||||
|
version "4.34.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.8.tgz#cbfee01f1fe73791c35191a05397838520ca3cdd"
|
||||||
|
integrity sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ==
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-ia32-msvc@4.34.8":
|
||||||
|
version "4.34.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.8.tgz#95cdbdff48fe6c948abcf6a1d500b2bd5ce33f62"
|
||||||
|
integrity sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w==
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-x64-msvc@4.34.8":
|
||||||
|
version "4.34.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.8.tgz#4cdb2cfae69cdb7b1a3cc58778e820408075e928"
|
||||||
|
integrity sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g==
|
||||||
|
|
||||||
"@rooks/use-did-update@^4.10.0":
|
"@rooks/use-did-update@^4.10.0":
|
||||||
version "4.11.2"
|
version "4.11.2"
|
||||||
@@ -3216,16 +3283,21 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/escodegen/-/escodegen-0.0.6.tgz#5230a9ce796e042cda6f086dbf19f22ea330659c"
|
resolved "https://registry.yarnpkg.com/@types/escodegen/-/escodegen-0.0.6.tgz#5230a9ce796e042cda6f086dbf19f22ea330659c"
|
||||||
integrity sha512-AjwI4MvWx3HAOaZqYsjKWyEObT9lcVV0Y0V8nXo6cXzN8ZiMxVhf6F3d/UNvXVGKrEzL/Dluc5p+y9GkzlTWig==
|
integrity sha512-AjwI4MvWx3HAOaZqYsjKWyEObT9lcVV0Y0V8nXo6cXzN8ZiMxVhf6F3d/UNvXVGKrEzL/Dluc5p+y9GkzlTWig==
|
||||||
|
|
||||||
"@types/estree@1.0.5", "@types/estree@^1.0.0":
|
"@types/estree@1.0.6":
|
||||||
version "1.0.5"
|
version "1.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
|
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
|
||||||
integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==
|
integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==
|
||||||
|
|
||||||
"@types/estree@^0.0.51":
|
"@types/estree@^0.0.51":
|
||||||
version "0.0.51"
|
version "0.0.51"
|
||||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40"
|
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40"
|
||||||
integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==
|
integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==
|
||||||
|
|
||||||
|
"@types/estree@^1.0.0":
|
||||||
|
version "1.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
|
||||||
|
integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==
|
||||||
|
|
||||||
"@types/express-serve-static-core@^4.17.33":
|
"@types/express-serve-static-core@^4.17.33":
|
||||||
version "4.17.42"
|
version "4.17.42"
|
||||||
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.42.tgz#2a276952acc73d1b8dc63fd4210647abbc553a71"
|
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.42.tgz#2a276952acc73d1b8dc63fd4210647abbc553a71"
|
||||||
@@ -3427,11 +3499,9 @@
|
|||||||
integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==
|
integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==
|
||||||
|
|
||||||
"@types/react-dom@^18.0.11":
|
"@types/react-dom@^18.0.11":
|
||||||
version "18.2.18"
|
version "18.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.18.tgz#16946e6cd43971256d874bc3d0a72074bb8571dd"
|
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.3.tgz#3654138d0da1b0c7916f6ed0dc1cc2b576d47650"
|
||||||
integrity sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==
|
integrity sha512-uTYkxTLkYp41nq/ULXyXMtkNT1vu5fXJoqad6uTNCOGat5t9cLgF4vMNLBXsTOXpdOI44XzKPY1M5RRm0bQHuw==
|
||||||
dependencies:
|
|
||||||
"@types/react" "*"
|
|
||||||
|
|
||||||
"@types/react-redux@^7.1.25":
|
"@types/react-redux@^7.1.25":
|
||||||
version "7.1.33"
|
version "7.1.33"
|
||||||
@@ -3467,7 +3537,7 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react@*", "@types/react@>=16", "@types/react@^18.0.28":
|
"@types/react@*", "@types/react@>=16":
|
||||||
version "18.2.48"
|
version "18.2.48"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.48.tgz#11df5664642d0bd879c1f58bc1d37205b064e8f1"
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.48.tgz#11df5664642d0bd879c1f58bc1d37205b064e8f1"
|
||||||
integrity sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w==
|
integrity sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w==
|
||||||
@@ -3476,6 +3546,14 @@
|
|||||||
"@types/scheduler" "*"
|
"@types/scheduler" "*"
|
||||||
csstype "^3.0.2"
|
csstype "^3.0.2"
|
||||||
|
|
||||||
|
"@types/react@^18.0.28":
|
||||||
|
version "18.3.14"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.14.tgz#7ce43bbca0e15e1c4f67ad33ea3f83d75aa6756b"
|
||||||
|
integrity sha512-NzahNKvjNhVjuPBQ+2G7WlxstQ+47kXZNHlUvFakDViuIEfGY926GqhMueQFZ7woG+sPiQKlF36XfrIUVSUfFg==
|
||||||
|
dependencies:
|
||||||
|
"@types/prop-types" "*"
|
||||||
|
csstype "^3.0.2"
|
||||||
|
|
||||||
"@types/resolve@1.20.2":
|
"@types/resolve@1.20.2":
|
||||||
version "1.20.2"
|
version "1.20.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975"
|
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975"
|
||||||
@@ -3996,10 +4074,10 @@ axios-rate-limit@^1.3.0:
|
|||||||
resolved "https://registry.yarnpkg.com/axios-rate-limit/-/axios-rate-limit-1.3.0.tgz#03241d24c231c47432dab6e8234cfde819253c2e"
|
resolved "https://registry.yarnpkg.com/axios-rate-limit/-/axios-rate-limit-1.3.0.tgz#03241d24c231c47432dab6e8234cfde819253c2e"
|
||||||
integrity sha512-cKR5wTbU/CeeyF1xVl5hl6FlYsmzDVqxlN4rGtfO5x7J83UxKDckudsW0yW21/ZJRcO0Qrfm3fUFbhEbWTLayw==
|
integrity sha512-cKR5wTbU/CeeyF1xVl5hl6FlYsmzDVqxlN4rGtfO5x7J83UxKDckudsW0yW21/ZJRcO0Qrfm3fUFbhEbWTLayw==
|
||||||
|
|
||||||
axios@^1.7.4:
|
axios@^1.8.2:
|
||||||
version "1.7.4"
|
version "1.8.2"
|
||||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.4.tgz#4c8ded1b43683c8dd362973c393f3ede24052aa2"
|
resolved "https://registry.yarnpkg.com/axios/-/axios-1.8.2.tgz#fabe06e241dfe83071d4edfbcaa7b1c3a40f7979"
|
||||||
integrity sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==
|
integrity sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==
|
||||||
dependencies:
|
dependencies:
|
||||||
follow-redirects "^1.15.6"
|
follow-redirects "^1.15.6"
|
||||||
form-data "^4.0.0"
|
form-data "^4.0.0"
|
||||||
@@ -4342,9 +4420,9 @@ camelize@^1.0.0:
|
|||||||
integrity sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==
|
integrity sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==
|
||||||
|
|
||||||
caniuse-lite@^1.0.30001565, caniuse-lite@^1.0.30001578:
|
caniuse-lite@^1.0.30001565, caniuse-lite@^1.0.30001578:
|
||||||
version "1.0.30001580"
|
version "1.0.30001700"
|
||||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001580.tgz#e3c76bc6fe020d9007647044278954ff8cd17d1e"
|
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001700.tgz"
|
||||||
integrity sha512-mtj5ur2FFPZcCEpXFy8ADXbDACuNFXg6mxVDqp7tqooX6l3zwm+d8EPoeOSIFRDvHs8qu7/SLFOGniULkcH2iA==
|
integrity sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==
|
||||||
|
|
||||||
catharsis@^0.9.0:
|
catharsis@^0.9.0:
|
||||||
version "0.9.0"
|
version "0.9.0"
|
||||||
@@ -4683,6 +4761,11 @@ cookie@0.6.0:
|
|||||||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051"
|
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051"
|
||||||
integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==
|
integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==
|
||||||
|
|
||||||
|
cookie@^1.0.1:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/cookie/-/cookie-1.0.2.tgz#27360701532116bd3f1f9416929d176afe1e4610"
|
||||||
|
integrity sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==
|
||||||
|
|
||||||
core-js-compat@^3.31.0, core-js-compat@^3.34.0:
|
core-js-compat@^3.31.0, core-js-compat@^3.34.0:
|
||||||
version "3.35.1"
|
version "3.35.1"
|
||||||
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.35.1.tgz#215247d7edb9e830efa4218ff719beb2803555e2"
|
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.35.1.tgz#215247d7edb9e830efa4218ff719beb2803555e2"
|
||||||
@@ -5470,34 +5553,34 @@ esbuild@^0.18.0:
|
|||||||
"@esbuild/win32-ia32" "0.18.20"
|
"@esbuild/win32-ia32" "0.18.20"
|
||||||
"@esbuild/win32-x64" "0.18.20"
|
"@esbuild/win32-x64" "0.18.20"
|
||||||
|
|
||||||
esbuild@^0.20.1:
|
esbuild@^0.21.3:
|
||||||
version "0.20.2"
|
version "0.21.5"
|
||||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.2.tgz#9d6b2386561766ee6b5a55196c6d766d28c87ea1"
|
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d"
|
||||||
integrity sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==
|
integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
"@esbuild/aix-ppc64" "0.20.2"
|
"@esbuild/aix-ppc64" "0.21.5"
|
||||||
"@esbuild/android-arm" "0.20.2"
|
"@esbuild/android-arm" "0.21.5"
|
||||||
"@esbuild/android-arm64" "0.20.2"
|
"@esbuild/android-arm64" "0.21.5"
|
||||||
"@esbuild/android-x64" "0.20.2"
|
"@esbuild/android-x64" "0.21.5"
|
||||||
"@esbuild/darwin-arm64" "0.20.2"
|
"@esbuild/darwin-arm64" "0.21.5"
|
||||||
"@esbuild/darwin-x64" "0.20.2"
|
"@esbuild/darwin-x64" "0.21.5"
|
||||||
"@esbuild/freebsd-arm64" "0.20.2"
|
"@esbuild/freebsd-arm64" "0.21.5"
|
||||||
"@esbuild/freebsd-x64" "0.20.2"
|
"@esbuild/freebsd-x64" "0.21.5"
|
||||||
"@esbuild/linux-arm" "0.20.2"
|
"@esbuild/linux-arm" "0.21.5"
|
||||||
"@esbuild/linux-arm64" "0.20.2"
|
"@esbuild/linux-arm64" "0.21.5"
|
||||||
"@esbuild/linux-ia32" "0.20.2"
|
"@esbuild/linux-ia32" "0.21.5"
|
||||||
"@esbuild/linux-loong64" "0.20.2"
|
"@esbuild/linux-loong64" "0.21.5"
|
||||||
"@esbuild/linux-mips64el" "0.20.2"
|
"@esbuild/linux-mips64el" "0.21.5"
|
||||||
"@esbuild/linux-ppc64" "0.20.2"
|
"@esbuild/linux-ppc64" "0.21.5"
|
||||||
"@esbuild/linux-riscv64" "0.20.2"
|
"@esbuild/linux-riscv64" "0.21.5"
|
||||||
"@esbuild/linux-s390x" "0.20.2"
|
"@esbuild/linux-s390x" "0.21.5"
|
||||||
"@esbuild/linux-x64" "0.20.2"
|
"@esbuild/linux-x64" "0.21.5"
|
||||||
"@esbuild/netbsd-x64" "0.20.2"
|
"@esbuild/netbsd-x64" "0.21.5"
|
||||||
"@esbuild/openbsd-x64" "0.20.2"
|
"@esbuild/openbsd-x64" "0.21.5"
|
||||||
"@esbuild/sunos-x64" "0.20.2"
|
"@esbuild/sunos-x64" "0.21.5"
|
||||||
"@esbuild/win32-arm64" "0.20.2"
|
"@esbuild/win32-arm64" "0.21.5"
|
||||||
"@esbuild/win32-ia32" "0.20.2"
|
"@esbuild/win32-ia32" "0.21.5"
|
||||||
"@esbuild/win32-x64" "0.20.2"
|
"@esbuild/win32-x64" "0.21.5"
|
||||||
|
|
||||||
escalade@^3.1.1:
|
escalade@^3.1.1:
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
@@ -6443,6 +6526,18 @@ graphemer@^1.4.0:
|
|||||||
resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
|
resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
|
||||||
integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
|
integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
|
||||||
|
|
||||||
|
graphql-request@^7.2.0:
|
||||||
|
version "7.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-7.2.0.tgz#af4aa25f27a087dd4fc93a4ff54a0f59c4487269"
|
||||||
|
integrity sha512-0GR7eQHBFYz372u9lxS16cOtEekFlZYB2qOyq8wDvzRmdRSJ0mgUVX1tzNcIzk3G+4NY+mGtSz411wZdeDF/+A==
|
||||||
|
dependencies:
|
||||||
|
"@graphql-typed-document-node/core" "^3.2.0"
|
||||||
|
|
||||||
|
graphql@^16.0.0:
|
||||||
|
version "16.11.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.11.0.tgz#96d17f66370678027fdf59b2d4c20b4efaa8a633"
|
||||||
|
integrity sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==
|
||||||
|
|
||||||
gunzip-maybe@^1.4.2:
|
gunzip-maybe@^1.4.2:
|
||||||
version "1.4.2"
|
version "1.4.2"
|
||||||
resolved "https://registry.yarnpkg.com/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz#b913564ae3be0eda6f3de36464837a9cd94b98ac"
|
resolved "https://registry.yarnpkg.com/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz#b913564ae3be0eda6f3de36464837a9cd94b98ac"
|
||||||
@@ -8182,11 +8277,16 @@ mz@^2.7.0:
|
|||||||
object-assign "^4.0.1"
|
object-assign "^4.0.1"
|
||||||
thenify-all "^1.0.0"
|
thenify-all "^1.0.0"
|
||||||
|
|
||||||
nanoid@^3.1.25, nanoid@^3.3.6, nanoid@^3.3.7:
|
nanoid@^3.1.25, nanoid@^3.3.6:
|
||||||
version "3.3.7"
|
version "3.3.7"
|
||||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
|
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
|
||||||
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
|
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
|
||||||
|
|
||||||
|
nanoid@^3.3.8:
|
||||||
|
version "3.3.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
|
||||||
|
integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==
|
||||||
|
|
||||||
natural-compare@^1.4.0:
|
natural-compare@^1.4.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||||
@@ -8685,6 +8785,11 @@ picocolors@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
|
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
|
||||||
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
|
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
|
||||||
|
|
||||||
|
picocolors@^1.1.1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
|
||||||
|
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
|
||||||
|
|
||||||
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3, picomatch@^2.3.0, picomatch@^2.3.1:
|
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3, picomatch@^2.3.0, picomatch@^2.3.1:
|
||||||
version "2.3.1"
|
version "2.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
||||||
@@ -8786,14 +8891,14 @@ postcss@8.4.31:
|
|||||||
picocolors "^1.0.0"
|
picocolors "^1.0.0"
|
||||||
source-map-js "^1.0.2"
|
source-map-js "^1.0.2"
|
||||||
|
|
||||||
postcss@^8.4.23, postcss@^8.4.32, postcss@^8.4.38:
|
postcss@^8.4.23, postcss@^8.4.32, postcss@^8.4.43:
|
||||||
version "8.4.38"
|
version "8.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e"
|
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.2.tgz#e7b99cb9d2ec3e8dd424002e7c16517cb2b846bd"
|
||||||
integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==
|
integrity sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==
|
||||||
dependencies:
|
dependencies:
|
||||||
nanoid "^3.3.7"
|
nanoid "^3.3.8"
|
||||||
picocolors "^1.0.0"
|
picocolors "^1.1.1"
|
||||||
source-map-js "^1.2.0"
|
source-map-js "^1.2.1"
|
||||||
|
|
||||||
prelude-ls@^1.2.1:
|
prelude-ls@^1.2.1:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
@@ -9070,12 +9175,12 @@ react-docgen@^7.0.0:
|
|||||||
strip-indent "^4.0.0"
|
strip-indent "^4.0.0"
|
||||||
|
|
||||||
react-dom@^18.2.0:
|
react-dom@^18.2.0:
|
||||||
version "18.2.0"
|
version "18.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
|
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4"
|
||||||
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
|
integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
scheduler "^0.23.0"
|
scheduler "^0.23.2"
|
||||||
|
|
||||||
react-element-to-jsx-string@^15.0.0:
|
react-element-to-jsx-string@^15.0.0:
|
||||||
version "15.0.0"
|
version "15.0.0"
|
||||||
@@ -9199,13 +9304,22 @@ react-router-dom@^6.9.0:
|
|||||||
"@remix-run/router" "1.14.2"
|
"@remix-run/router" "1.14.2"
|
||||||
react-router "6.21.3"
|
react-router "6.21.3"
|
||||||
|
|
||||||
react-router@6.21.3, react-router@^6.9.0:
|
react-router@6.21.3:
|
||||||
version "6.21.3"
|
version "6.21.3"
|
||||||
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.21.3.tgz#8086cea922c2bfebbb49c6594967418f1f167d70"
|
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.21.3.tgz#8086cea922c2bfebbb49c6594967418f1f167d70"
|
||||||
integrity sha512-a0H638ZXULv1OdkmiK6s6itNhoy33ywxmUFT/xtSoVyf9VnC7n7+VT4LjVzdIHSaF5TIh9ylUgxMXksHTgGrKg==
|
integrity sha512-a0H638ZXULv1OdkmiK6s6itNhoy33ywxmUFT/xtSoVyf9VnC7n7+VT4LjVzdIHSaF5TIh9ylUgxMXksHTgGrKg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@remix-run/router" "1.14.2"
|
"@remix-run/router" "1.14.2"
|
||||||
|
|
||||||
|
react-router@^7.5.2:
|
||||||
|
version "7.5.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.5.2.tgz#889137b9ce91dadc06eb40997f57f08199845973"
|
||||||
|
integrity sha512-9Rw8r199klMnlGZ8VAsV/I8WrIF6IyJ90JQUdboupx1cdkgYqwnrYjH+I/nY/7cA1X5zia4mDJqH36npP7sxGQ==
|
||||||
|
dependencies:
|
||||||
|
cookie "^1.0.1"
|
||||||
|
set-cookie-parser "^2.6.0"
|
||||||
|
turbo-stream "2.4.0"
|
||||||
|
|
||||||
react-select-async-paginate@^0.7.2:
|
react-select-async-paginate@^0.7.2:
|
||||||
version "0.7.3"
|
version "0.7.3"
|
||||||
resolved "https://registry.yarnpkg.com/react-select-async-paginate/-/react-select-async-paginate-0.7.3.tgz#d2d718aede42e884d9aed627b7a65b32f3813642"
|
resolved "https://registry.yarnpkg.com/react-select-async-paginate/-/react-select-async-paginate-0.7.3.tgz#d2d718aede42e884d9aed627b7a65b32f3813642"
|
||||||
@@ -9558,28 +9672,32 @@ rimraf@~2.6.2:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents "~2.3.2"
|
fsevents "~2.3.2"
|
||||||
|
|
||||||
rollup@^4.13.0:
|
rollup@^4.20.0:
|
||||||
version "4.13.2"
|
version "4.34.8"
|
||||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.13.2.tgz#ac57d2dc48e8f5562f5a6daadb9caee590069262"
|
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.34.8.tgz#e859c1a51d899aba9bcf451d4eed1d11fb8e2a6e"
|
||||||
integrity sha512-MIlLgsdMprDBXC+4hsPgzWUasLO9CE4zOkj/u6j+Z6j5A4zRY+CtiXAdJyPtgCsc42g658Aeh1DlrdVEJhsL2g==
|
integrity sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/estree" "1.0.5"
|
"@types/estree" "1.0.6"
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
"@rollup/rollup-android-arm-eabi" "4.13.2"
|
"@rollup/rollup-android-arm-eabi" "4.34.8"
|
||||||
"@rollup/rollup-android-arm64" "4.13.2"
|
"@rollup/rollup-android-arm64" "4.34.8"
|
||||||
"@rollup/rollup-darwin-arm64" "4.13.2"
|
"@rollup/rollup-darwin-arm64" "4.34.8"
|
||||||
"@rollup/rollup-darwin-x64" "4.13.2"
|
"@rollup/rollup-darwin-x64" "4.34.8"
|
||||||
"@rollup/rollup-linux-arm-gnueabihf" "4.13.2"
|
"@rollup/rollup-freebsd-arm64" "4.34.8"
|
||||||
"@rollup/rollup-linux-arm64-gnu" "4.13.2"
|
"@rollup/rollup-freebsd-x64" "4.34.8"
|
||||||
"@rollup/rollup-linux-arm64-musl" "4.13.2"
|
"@rollup/rollup-linux-arm-gnueabihf" "4.34.8"
|
||||||
"@rollup/rollup-linux-powerpc64le-gnu" "4.13.2"
|
"@rollup/rollup-linux-arm-musleabihf" "4.34.8"
|
||||||
"@rollup/rollup-linux-riscv64-gnu" "4.13.2"
|
"@rollup/rollup-linux-arm64-gnu" "4.34.8"
|
||||||
"@rollup/rollup-linux-s390x-gnu" "4.13.2"
|
"@rollup/rollup-linux-arm64-musl" "4.34.8"
|
||||||
"@rollup/rollup-linux-x64-gnu" "4.13.2"
|
"@rollup/rollup-linux-loongarch64-gnu" "4.34.8"
|
||||||
"@rollup/rollup-linux-x64-musl" "4.13.2"
|
"@rollup/rollup-linux-powerpc64le-gnu" "4.34.8"
|
||||||
"@rollup/rollup-win32-arm64-msvc" "4.13.2"
|
"@rollup/rollup-linux-riscv64-gnu" "4.34.8"
|
||||||
"@rollup/rollup-win32-ia32-msvc" "4.13.2"
|
"@rollup/rollup-linux-s390x-gnu" "4.34.8"
|
||||||
"@rollup/rollup-win32-x64-msvc" "4.13.2"
|
"@rollup/rollup-linux-x64-gnu" "4.34.8"
|
||||||
|
"@rollup/rollup-linux-x64-musl" "4.34.8"
|
||||||
|
"@rollup/rollup-win32-arm64-msvc" "4.34.8"
|
||||||
|
"@rollup/rollup-win32-ia32-msvc" "4.34.8"
|
||||||
|
"@rollup/rollup-win32-x64-msvc" "4.34.8"
|
||||||
fsevents "~2.3.2"
|
fsevents "~2.3.2"
|
||||||
|
|
||||||
run-parallel@^1.1.9:
|
run-parallel@^1.1.9:
|
||||||
@@ -9632,10 +9750,10 @@ sass@^1.77.0:
|
|||||||
immutable "^4.0.0"
|
immutable "^4.0.0"
|
||||||
source-map-js ">=0.6.2 <2.0.0"
|
source-map-js ">=0.6.2 <2.0.0"
|
||||||
|
|
||||||
scheduler@^0.23.0:
|
scheduler@^0.23.2:
|
||||||
version "0.23.0"
|
version "0.23.2"
|
||||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe"
|
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3"
|
||||||
integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==
|
integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
|
|
||||||
@@ -9716,6 +9834,11 @@ serve-static@1.16.0:
|
|||||||
parseurl "~1.3.3"
|
parseurl "~1.3.3"
|
||||||
send "0.18.0"
|
send "0.18.0"
|
||||||
|
|
||||||
|
set-cookie-parser@^2.6.0:
|
||||||
|
version "2.7.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz#3016f150072202dfbe90fadee053573cc89d2943"
|
||||||
|
integrity sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==
|
||||||
|
|
||||||
set-function-length@^1.1.1:
|
set-function-length@^1.1.1:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.0.tgz#2f81dc6c16c7059bda5ab7c82c11f03a515ed8e1"
|
resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.0.tgz#2f81dc6c16c7059bda5ab7c82c11f03a515ed8e1"
|
||||||
@@ -9851,10 +9974,10 @@ socket.io-parser@~4.2.4:
|
|||||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
|
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
|
||||||
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
|
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
|
||||||
|
|
||||||
source-map-js@^1.2.0:
|
source-map-js@^1.2.1:
|
||||||
version "1.2.0"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af"
|
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
|
||||||
integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==
|
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
|
||||||
|
|
||||||
source-map-support@0.5.13:
|
source-map-support@0.5.13:
|
||||||
version "0.5.13"
|
version "0.5.13"
|
||||||
@@ -10192,9 +10315,9 @@ tailwindcss@^3.4.1:
|
|||||||
sucrase "^3.32.0"
|
sucrase "^3.32.0"
|
||||||
|
|
||||||
tar-fs@^2.1.1:
|
tar-fs@^2.1.1:
|
||||||
version "2.1.1"
|
version "2.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784"
|
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.3.tgz#fb3b8843a26b6f13a08e606f7922875eb1fbbf92"
|
||||||
integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==
|
integrity sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==
|
||||||
dependencies:
|
dependencies:
|
||||||
chownr "^1.1.1"
|
chownr "^1.1.1"
|
||||||
mkdirp-classic "^0.5.2"
|
mkdirp-classic "^0.5.2"
|
||||||
@@ -10400,6 +10523,11 @@ tui-jsdoc-template@^1.2.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
cheerio "^0.22.0"
|
cheerio "^0.22.0"
|
||||||
|
|
||||||
|
turbo-stream@2.4.0:
|
||||||
|
version "2.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/turbo-stream/-/turbo-stream-2.4.0.tgz#1e4fca6725e90fa14ac4adb782f2d3759a5695f0"
|
||||||
|
integrity sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==
|
||||||
|
|
||||||
tween-functions@^1.2.0:
|
tween-functions@^1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/tween-functions/-/tween-functions-1.2.0.tgz#1ae3a50e7c60bb3def774eac707acbca73bbc3ff"
|
resolved "https://registry.yarnpkg.com/tween-functions/-/tween-functions-1.2.0.tgz#1ae3a50e7c60bb3def774eac707acbca73bbc3ff"
|
||||||
@@ -10780,14 +10908,14 @@ vite-plugin-html@^3.2.0:
|
|||||||
node-html-parser "^5.3.3"
|
node-html-parser "^5.3.3"
|
||||||
pathe "^0.2.0"
|
pathe "^0.2.0"
|
||||||
|
|
||||||
vite@^5.2.14:
|
vite@^5.4.19:
|
||||||
version "5.2.14"
|
version "5.4.19"
|
||||||
resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.14.tgz#fd5f60facf6b5f90ec7da6323c467a365d380c3d"
|
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.19.tgz#20efd060410044b3ed555049418a5e7d1998f959"
|
||||||
integrity sha512-TFQLuwWLPms+NBNlh0D9LZQ+HXW471COABxw/9TEUBrjuHMo9BrYBPrN/SYAwIuVL+rLerycxiLT41t4f5MZpA==
|
integrity sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild "^0.20.1"
|
esbuild "^0.21.3"
|
||||||
postcss "^8.4.38"
|
postcss "^8.4.43"
|
||||||
rollup "^4.13.0"
|
rollup "^4.20.0"
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents "~2.3.3"
|
fsevents "~2.3.3"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user