🏗️ Cleaning up forms and cards
This commit is contained in:
@@ -11,7 +11,7 @@ import {
|
||||
getComicBooks,
|
||||
} from "../../actions/fileops.actions";
|
||||
import { getLibraryStatistics } from "../../actions/comicinfo.actions";
|
||||
import { isEmpty, isNil } from "lodash";
|
||||
import { isEmpty, isNil, isUndefined } from "lodash";
|
||||
import Header from "../shared/Header";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import axios from "axios";
|
||||
@@ -20,6 +20,10 @@ import {
|
||||
LIBRARY_SERVICE_BASE_URI,
|
||||
LIBRARY_SERVICE_HOST,
|
||||
} from "../../constants/endpoints";
|
||||
import {
|
||||
determineCoverFile,
|
||||
determineExternalMetadata,
|
||||
} from "../../shared/utils/metadata.utils";
|
||||
|
||||
export const Dashboard = (): ReactElement => {
|
||||
const { data: recentComics } = useQuery({
|
||||
@@ -86,71 +90,97 @@ export const Dashboard = (): ReactElement => {
|
||||
<section>
|
||||
<h1>Dashboard</h1>
|
||||
<div className="grid grid-cols-5 gap-6">
|
||||
{recentComics?.data.docs.map((recentComic, idx) => (
|
||||
<Card
|
||||
orientation="horizontal-2-medium"
|
||||
key={idx}
|
||||
imageUrl={`${LIBRARY_SERVICE_HOST}/${recentComic.rawFileDetails.cover.filePath}`}
|
||||
title={recentComic.inferredMetadata.issue.name}
|
||||
hasDetails
|
||||
>
|
||||
<div>
|
||||
<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">
|
||||
<span className="pr-1 pt-1">
|
||||
<i className="icon-[solar--hashtag-outline]"></i>
|
||||
</span>
|
||||
<span className="text-md text-slate-500">
|
||||
{recentComic.inferredMetadata.issue.number}
|
||||
</span>
|
||||
</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--file-bold-duotone] w-4 h-4"></i>
|
||||
</span>
|
||||
{recentComics?.data.docs.map(
|
||||
(
|
||||
{
|
||||
_id,
|
||||
rawFileDetails,
|
||||
sourcedMetadata: { comicvine, comicInfo, locg },
|
||||
inferredMetadata,
|
||||
acquisition: {
|
||||
source: { name },
|
||||
},
|
||||
},
|
||||
idx,
|
||||
) => {
|
||||
const { issueName, url } = determineCoverFile({
|
||||
rawFileDetails,
|
||||
comicvine,
|
||||
comicInfo,
|
||||
locg,
|
||||
});
|
||||
const { issue, coverURL, icon } = determineExternalMetadata(
|
||||
name,
|
||||
{
|
||||
comicvine,
|
||||
comicInfo,
|
||||
locg,
|
||||
},
|
||||
);
|
||||
const isComicVineMetadataAvailable =
|
||||
!isUndefined(comicvine) &&
|
||||
!isUndefined(comicvine.volumeInformation);
|
||||
|
||||
<span className="text-md text-slate-500">
|
||||
{recentComic.rawFileDetails.extension}
|
||||
</span>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
return (
|
||||
<Card
|
||||
orientation="vertical-2"
|
||||
key={idx}
|
||||
imageUrl={`${LIBRARY_SERVICE_HOST}/${rawFileDetails.cover.filePath}`}
|
||||
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">
|
||||
<div className="sm:inline-flex sm:shrink-0 sm:items-center sm:gap-2">
|
||||
<i className="h-7 w-7 icon-[solar--code-file-bold-duotone] text-orange-400 dark:text-orange"></i>
|
||||
<span className="text-md text-slate-500 dark:text-slate-900">
|
||||
{rawFileDetails.extension}
|
||||
</span>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
{/* <div className="">
|
||||
<p className="text-gray-500">Parking</p>
|
||||
<p className="font-medium">2 spaces</p>
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
<div className="sm:inline-flex sm:shrink-0 sm:items-center sm:gap-2">
|
||||
<svg
|
||||
className="h-4 w-4 text-indigo-700"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
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"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
{/* <div className="mt-1.5 sm:mt-0">
|
||||
<p className="text-slate-500">Bathroom</p>
|
||||
<p className="font-medium">2 rooms</p>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
<div className="flex flex-row items-center gap-1 my-2">
|
||||
<div className="sm:inline-flex sm:shrink-0 sm:items-center sm:gap-2">
|
||||
{/* ComicInfo.xml presence */}
|
||||
{!isNil(comicInfo) && !isEmpty(comicInfo) && (
|
||||
<i className="h-7 w-7 icon-[solar--code-file-bold-duotone] text-slate-500 dark:text-slate-300"></i>
|
||||
)}
|
||||
{/* ComicVine metadata presence */}
|
||||
{isComicVineMetadataAvailable && (
|
||||
<span className="w-7 h-7">
|
||||
<img
|
||||
src="/src/client/assets/img/cvlogo.svg"
|
||||
alt={"ComicVine metadata detected."}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* Raw file presence */}
|
||||
{isNil(rawFileDetails) && (
|
||||
<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" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -71,7 +71,6 @@ export const AirDCPPSettingsForm = (): ReactElement => {
|
||||
const initFormData = !isUndefined(airDCPPClientConfiguration)
|
||||
? airDCPPClientConfiguration
|
||||
: {};
|
||||
console.log(airDCPPClientConfiguration);
|
||||
return (
|
||||
<>
|
||||
<ConnectionForm
|
||||
|
||||
@@ -50,24 +50,42 @@ export const Settings = (props: ISettingsProps): ReactElement => {
|
||||
},
|
||||
];
|
||||
return (
|
||||
<section className="container">
|
||||
<div className="columns">
|
||||
<div className="section column is-one-quarter">
|
||||
<h1 className="title">Settings</h1>
|
||||
<aside className="menu">
|
||||
<div>
|
||||
<section>
|
||||
<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="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) => {
|
||||
return (
|
||||
<div key={idx}>
|
||||
<p className="menu-label">{settingObject.category}</p>
|
||||
<div className="w-64 py-2 text-slate" key={idx}>
|
||||
<h3 className="text-l pb-2">
|
||||
{settingObject.category.toUpperCase()}
|
||||
</h3>
|
||||
{/* First level children */}
|
||||
{!isUndefined(settingObject.children) ? (
|
||||
<ul className="menu-list" key={settingObject.id}>
|
||||
<ul key={settingObject.id}>
|
||||
{map(settingObject.children, (item, idx) => {
|
||||
return (
|
||||
<li key={idx}>
|
||||
<li key={idx} className="mb-2">
|
||||
<a
|
||||
className={
|
||||
item.id.toString() === active ? "is-active" : ""
|
||||
item.id.toString() === active
|
||||
? "is-active flex items-center"
|
||||
: "flex items-center"
|
||||
}
|
||||
onClick={() => setActive(item.id.toString())}
|
||||
>
|
||||
@@ -75,14 +93,14 @@ export const Settings = (props: ISettingsProps): ReactElement => {
|
||||
</a>
|
||||
{/* Second level children */}
|
||||
{!isUndefined(item.children) ? (
|
||||
<ul>
|
||||
<ul className="pl-4">
|
||||
{map(item.children, (item, idx) => (
|
||||
<li key={item.id}>
|
||||
<li key={item.id} className="mb-2">
|
||||
<a
|
||||
className={
|
||||
item.id.toString() === active
|
||||
? "is-active"
|
||||
: ""
|
||||
? "is-active flex items-center"
|
||||
: "flex items-center"
|
||||
}
|
||||
onClick={() =>
|
||||
setActive(item.id.toString())
|
||||
@@ -103,18 +121,18 @@ export const Settings = (props: ISettingsProps): ReactElement => {
|
||||
);
|
||||
})}
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
{/* content for settings */}
|
||||
<div className="section column is-half mt-6">
|
||||
<div className="content">
|
||||
{map(settingsContent, ({ id, content }) =>
|
||||
active === id ? content : null,
|
||||
)}
|
||||
{/* content for settings */}
|
||||
<div className="max-w-screen-xl">
|
||||
<div className="content">
|
||||
{map(settingsContent, ({ id, content }) =>
|
||||
active === id ? content : null,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -104,24 +104,23 @@ const renderCard = (props: ICardProps): ReactElement => {
|
||||
</div>
|
||||
);
|
||||
|
||||
case "horizontal-2-small":
|
||||
case "horizontal-small":
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row justify-start align-top gap-3 bg-slate-200 h-fit rounded-md shadow-md shadow-white-400">
|
||||
{/* 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" />
|
||||
</div>
|
||||
{/* details */}
|
||||
<div className="w-fit h-fit pl-1 pr-2 py-1">
|
||||
<p className="text-sm">{props.title}</p>
|
||||
nothin
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
case "horizontal-2-medium":
|
||||
case "horizontal-medium":
|
||||
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">
|
||||
@@ -137,6 +136,17 @@ const renderCard = (props: ICardProps): ReactElement => {
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
case "cover-only":
|
||||
return (
|
||||
<>
|
||||
{/* thumbnail */}
|
||||
<div className="rounded-md overflow-hidden w-fit h-fit">
|
||||
<img src={props.imageUrl} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,56 @@ export const ConnectionForm = ({
|
||||
initialValues={initialData}
|
||||
render={({ 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>
|
||||
<div className="field has-addons">
|
||||
<p className="control">
|
||||
|
||||
Reference in New Issue
Block a user