🏗️ Cleaning up forms and cards
This commit is contained in:
@@ -11,7 +11,7 @@ import {
|
|||||||
getComicBooks,
|
getComicBooks,
|
||||||
} from "../../actions/fileops.actions";
|
} from "../../actions/fileops.actions";
|
||||||
import { getLibraryStatistics } from "../../actions/comicinfo.actions";
|
import { getLibraryStatistics } from "../../actions/comicinfo.actions";
|
||||||
import { isEmpty, isNil } from "lodash";
|
import { isEmpty, isNil, isUndefined } from "lodash";
|
||||||
import Header from "../shared/Header";
|
import Header from "../shared/Header";
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
@@ -20,6 +20,10 @@ import {
|
|||||||
LIBRARY_SERVICE_BASE_URI,
|
LIBRARY_SERVICE_BASE_URI,
|
||||||
LIBRARY_SERVICE_HOST,
|
LIBRARY_SERVICE_HOST,
|
||||||
} from "../../constants/endpoints";
|
} from "../../constants/endpoints";
|
||||||
|
import {
|
||||||
|
determineCoverFile,
|
||||||
|
determineExternalMetadata,
|
||||||
|
} from "../../shared/utils/metadata.utils";
|
||||||
|
|
||||||
export const Dashboard = (): ReactElement => {
|
export const Dashboard = (): ReactElement => {
|
||||||
const { data: recentComics } = useQuery({
|
const { data: recentComics } = useQuery({
|
||||||
@@ -86,71 +90,97 @@ export const Dashboard = (): ReactElement => {
|
|||||||
<section>
|
<section>
|
||||||
<h1>Dashboard</h1>
|
<h1>Dashboard</h1>
|
||||||
<div className="grid grid-cols-5 gap-6">
|
<div className="grid grid-cols-5 gap-6">
|
||||||
{recentComics?.data.docs.map((recentComic, idx) => (
|
{recentComics?.data.docs.map(
|
||||||
<Card
|
(
|
||||||
orientation="horizontal-2-medium"
|
{
|
||||||
key={idx}
|
_id,
|
||||||
imageUrl={`${LIBRARY_SERVICE_HOST}/${recentComic.rawFileDetails.cover.filePath}`}
|
rawFileDetails,
|
||||||
title={recentComic.inferredMetadata.issue.name}
|
sourcedMetadata: { comicvine, comicInfo, locg },
|
||||||
hasDetails
|
inferredMetadata,
|
||||||
>
|
acquisition: {
|
||||||
<div>
|
source: { name },
|
||||||
<dt className="sr-only">Address</dt>
|
},
|
||||||
<dd className="text-sm my-1 flex flex-row gap-1">
|
},
|
||||||
<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">
|
idx,
|
||||||
<span className="pr-1 pt-1">
|
) => {
|
||||||
<i className="icon-[solar--hashtag-outline]"></i>
|
const { issueName, url } = determineCoverFile({
|
||||||
</span>
|
rawFileDetails,
|
||||||
<span className="text-md text-slate-500">
|
comicvine,
|
||||||
{recentComic.inferredMetadata.issue.number}
|
comicInfo,
|
||||||
</span>
|
locg,
|
||||||
</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">
|
const { issue, coverURL, icon } = determineExternalMetadata(
|
||||||
<span className="pr-1 pt-1">
|
name,
|
||||||
<i className="icon-[solar--file-bold-duotone] w-4 h-4"></i>
|
{
|
||||||
</span>
|
comicvine,
|
||||||
|
comicInfo,
|
||||||
|
locg,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const isComicVineMetadataAvailable =
|
||||||
|
!isUndefined(comicvine) &&
|
||||||
|
!isUndefined(comicvine.volumeInformation);
|
||||||
|
|
||||||
<span className="text-md text-slate-500">
|
return (
|
||||||
{recentComic.rawFileDetails.extension}
|
<Card
|
||||||
</span>
|
orientation="vertical-2"
|
||||||
</span>
|
key={idx}
|
||||||
</dd>
|
imageUrl={`${LIBRARY_SERVICE_HOST}/${rawFileDetails.cover.filePath}`}
|
||||||
</div>
|
title={inferredMetadata.issue.name}
|
||||||
|
hasDetails
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<dt className="sr-only">Address</dt>
|
||||||
|
<dd className="text-sm my-1 flex flex-row gap-1">
|
||||||
|
{/* Issue number */}
|
||||||
|
<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-outline]"></i>
|
||||||
|
</span>
|
||||||
|
<span className="text-md text-slate-900">
|
||||||
|
{inferredMetadata.issue.number}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
{/* File extension */}
|
||||||
|
<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--file-bold-duotone] w-4 h-4"></i>
|
||||||
|
</span>
|
||||||
|
|
||||||
<div className="flex flex-row items-center gap-4 my-2">
|
<span className="text-md text-slate-500 dark:text-slate-900">
|
||||||
<div className="sm:inline-flex sm:shrink-0 sm:items-center sm:gap-2">
|
{rawFileDetails.extension}
|
||||||
<i className="h-7 w-7 icon-[solar--code-file-bold-duotone] text-orange-400 dark:text-orange"></i>
|
</span>
|
||||||
|
</span>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* <div className="">
|
<div className="flex flex-row items-center gap-1 my-2">
|
||||||
<p className="text-gray-500">Parking</p>
|
<div className="sm:inline-flex sm:shrink-0 sm:items-center sm:gap-2">
|
||||||
<p className="font-medium">2 spaces</p>
|
{/* ComicInfo.xml presence */}
|
||||||
</div> */}
|
{!isNil(comicInfo) && !isEmpty(comicInfo) && (
|
||||||
</div>
|
<i className="h-7 w-7 icon-[solar--code-file-bold-duotone] text-slate-500 dark:text-slate-300"></i>
|
||||||
|
)}
|
||||||
<div className="sm:inline-flex sm:shrink-0 sm:items-center sm:gap-2">
|
{/* ComicVine metadata presence */}
|
||||||
<svg
|
{isComicVineMetadataAvailable && (
|
||||||
className="h-4 w-4 text-indigo-700"
|
<span className="w-7 h-7">
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<img
|
||||||
fill="none"
|
src="/src/client/assets/img/cvlogo.svg"
|
||||||
viewBox="0 0 24 24"
|
alt={"ComicVine metadata detected."}
|
||||||
stroke="currentColor"
|
/>
|
||||||
>
|
</span>
|
||||||
<path
|
)}
|
||||||
strokeLinecap="round"
|
</div>
|
||||||
strokeLinejoin="round"
|
{/* Raw file presence */}
|
||||||
strokeWidth="2"
|
{isNil(rawFileDetails) && (
|
||||||
d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"
|
<span className="h-6 w-5 sm:shrink-0 sm:items-center sm:gap-2">
|
||||||
/>
|
<i className="icon-[solar--file-corrupted-outline] h-5 w-5" />
|
||||||
</svg>
|
</span>
|
||||||
|
)}
|
||||||
{/* <div className="mt-1.5 sm:mt-0">
|
</div>
|
||||||
<p className="text-slate-500">Bathroom</p>
|
</Card>
|
||||||
<p className="font-medium">2 rooms</p>
|
);
|
||||||
</div> */}
|
},
|
||||||
</div>
|
)}
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ export const AirDCPPSettingsForm = (): ReactElement => {
|
|||||||
const initFormData = !isUndefined(airDCPPClientConfiguration)
|
const initFormData = !isUndefined(airDCPPClientConfiguration)
|
||||||
? airDCPPClientConfiguration
|
? airDCPPClientConfiguration
|
||||||
: {};
|
: {};
|
||||||
console.log(airDCPPClientConfiguration);
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ConnectionForm
|
<ConnectionForm
|
||||||
|
|||||||
@@ -50,24 +50,42 @@ export const Settings = (props: ISettingsProps): ReactElement => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<section className="container">
|
<div>
|
||||||
<div className="columns">
|
<section>
|
||||||
<div className="section column is-one-quarter">
|
<header className="bg-slate-200 dark:bg-slate-500">
|
||||||
<h1 className="title">Settings</h1>
|
<div className="mx-auto max-w-screen-xl px-2 py-2 sm:px-6 sm:py-8 lg:px-8 lg:py-4">
|
||||||
<aside className="menu">
|
<div className="sm:flex sm:items-center sm:justify-between">
|
||||||
|
<div className="text-center sm:text-left">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white sm:text-3xl">
|
||||||
|
Settings
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p className="mt-1.5 text-sm text-gray-500 dark:text-white">
|
||||||
|
Import comics into the ThreeTwo library.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div className="flex flex-cols max-w-screen-xl mx-auto">
|
||||||
|
<aside className="px-4 py-4 sm:px-6 sm:py-8 lg:px-8">
|
||||||
{map(settingsObject, (settingObject, idx) => {
|
{map(settingsObject, (settingObject, idx) => {
|
||||||
return (
|
return (
|
||||||
<div key={idx}>
|
<div className="w-64 py-2 text-slate" key={idx}>
|
||||||
<p className="menu-label">{settingObject.category}</p>
|
<h3 className="text-l pb-2">
|
||||||
|
{settingObject.category.toUpperCase()}
|
||||||
|
</h3>
|
||||||
{/* First level children */}
|
{/* First level children */}
|
||||||
{!isUndefined(settingObject.children) ? (
|
{!isUndefined(settingObject.children) ? (
|
||||||
<ul className="menu-list" key={settingObject.id}>
|
<ul key={settingObject.id}>
|
||||||
{map(settingObject.children, (item, idx) => {
|
{map(settingObject.children, (item, idx) => {
|
||||||
return (
|
return (
|
||||||
<li key={idx}>
|
<li key={idx} className="mb-2">
|
||||||
<a
|
<a
|
||||||
className={
|
className={
|
||||||
item.id.toString() === active ? "is-active" : ""
|
item.id.toString() === active
|
||||||
|
? "is-active flex items-center"
|
||||||
|
: "flex items-center"
|
||||||
}
|
}
|
||||||
onClick={() => setActive(item.id.toString())}
|
onClick={() => setActive(item.id.toString())}
|
||||||
>
|
>
|
||||||
@@ -75,14 +93,14 @@ export const Settings = (props: ISettingsProps): ReactElement => {
|
|||||||
</a>
|
</a>
|
||||||
{/* Second level children */}
|
{/* Second level children */}
|
||||||
{!isUndefined(item.children) ? (
|
{!isUndefined(item.children) ? (
|
||||||
<ul>
|
<ul className="pl-4">
|
||||||
{map(item.children, (item, idx) => (
|
{map(item.children, (item, idx) => (
|
||||||
<li key={item.id}>
|
<li key={item.id} className="mb-2">
|
||||||
<a
|
<a
|
||||||
className={
|
className={
|
||||||
item.id.toString() === active
|
item.id.toString() === active
|
||||||
? "is-active"
|
? "is-active flex items-center"
|
||||||
: ""
|
: "flex items-center"
|
||||||
}
|
}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setActive(item.id.toString())
|
setActive(item.id.toString())
|
||||||
@@ -103,18 +121,18 @@ export const Settings = (props: ISettingsProps): ReactElement => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* content for settings */}
|
{/* content for settings */}
|
||||||
<div className="section column is-half mt-6">
|
<div className="max-w-screen-xl">
|
||||||
<div className="content">
|
<div className="content">
|
||||||
{map(settingsContent, ({ id, content }) =>
|
{map(settingsContent, ({ id, content }) =>
|
||||||
active === id ? content : null,
|
active === id ? content : null,
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</section>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -104,24 +104,23 @@ const renderCard = (props: ICardProps): ReactElement => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
case "horizontal-2-small":
|
case "horizontal-small":
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-row justify-start align-top gap-3 bg-slate-200 h-fit rounded-md shadow-md shadow-white-400">
|
<div className="flex flex-row justify-start align-top gap-3 bg-slate-200 h-fit rounded-md shadow-md shadow-white-400">
|
||||||
{/* thumbnail */}
|
{/* thumbnail */}
|
||||||
<div className="rounded-l-md overflow-hidden">
|
<div className="rounded-md overflow-hidden">
|
||||||
<img src={props.imageUrl} className="object-cover h-20 w-20" />
|
<img src={props.imageUrl} className="object-cover h-20 w-20" />
|
||||||
</div>
|
</div>
|
||||||
{/* details */}
|
{/* details */}
|
||||||
<div className="w-fit h-fit pl-1 pr-2 py-1">
|
<div className="w-fit h-fit pl-1 pr-2 py-1">
|
||||||
<p className="text-sm">{props.title}</p>
|
<p className="text-sm">{props.title}</p>
|
||||||
nothin
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
case "horizontal-2-medium":
|
case "horizontal-medium":
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-row items-center align-top gap-3 bg-slate-200 h-fit p-2 rounded-md shadow-md shadow-white-400">
|
<div className="flex flex-row items-center align-top gap-3 bg-slate-200 h-fit p-2 rounded-md shadow-md shadow-white-400">
|
||||||
@@ -137,6 +136,17 @@ const renderCard = (props: ICardProps): ReactElement => {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case "cover-only":
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* thumbnail */}
|
||||||
|
<div className="rounded-md overflow-hidden w-fit h-fit">
|
||||||
|
<img src={props.imageUrl} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,56 @@ export const ConnectionForm = ({
|
|||||||
initialValues={initialData}
|
initialValues={initialData}
|
||||||
render={({ handleSubmit }) => (
|
render={({ handleSubmit }) => (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<h2>{formHeading}</h2>
|
<h2 className="text-xl">{formHeading}</h2>
|
||||||
|
|
||||||
|
<div className="relative flex w-full max-w-[24rem]">
|
||||||
|
<Field name="hostname" validate={hostNameValidator}>
|
||||||
|
{({ input, meta }) => (
|
||||||
|
<div className="flex items-center rounded-md border border-gray-300">
|
||||||
|
<div className="relative">
|
||||||
|
{/* <select
|
||||||
|
id="dropdown"
|
||||||
|
className="appearance-none h-11 bg-transparent rounded-none border-r border-gray-300 text-gray-700 dark:text-slate-200 py-1 px-3 sm:text-sm sm:leading-5 focus:outline-none focus:shadow-outline-blue focus:border-blue-300"
|
||||||
|
>
|
||||||
|
<option>Protocol</option>
|
||||||
|
<option value="http">http://</option>
|
||||||
|
<option value="https">https://</option>
|
||||||
|
</select> */}
|
||||||
|
<Field
|
||||||
|
name="protocol"
|
||||||
|
component="select"
|
||||||
|
className="appearance-none h-11 bg-transparent rounded-none border-r border-gray-300 text-gray-700 dark:text-slate-200 py-1 px-3 sm:text-sm sm:leading-5 focus:outline-none focus:shadow-outline-blue focus:border-blue-300"
|
||||||
|
>
|
||||||
|
<option>Protocol</option>
|
||||||
|
<option value="http">http://</option>
|
||||||
|
<option value="https">https://</option>
|
||||||
|
</Field>
|
||||||
|
<div className="absolute right-0 inset-y-0 flex items-center px-0 pointer-events-none">
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5 text-gray-400"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path d="M7 7l3-3 3 3m0 6l-3 3-3-3"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
{...input}
|
||||||
|
type="text"
|
||||||
|
placeholder="hostname"
|
||||||
|
className="ml-2 bg-transparent py-2 px-2 block w-full rounded-md sm:text-sm sm:leading-5 focus:outline-none focus:shadow-outline-blue focus:border-blue-300"
|
||||||
|
/>
|
||||||
|
{meta.error && meta.touched && (
|
||||||
|
<span className="is-size-7 has-text-danger">
|
||||||
|
{meta.error}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
<label className="label">Hostname</label>
|
<label className="label">Hostname</label>
|
||||||
<div className="field has-addons">
|
<div className="field has-addons">
|
||||||
<p className="control">
|
<p className="control">
|
||||||
|
|||||||
Reference in New Issue
Block a user