🏗️ Cleaning up forms and cards

This commit is contained in:
2023-12-13 12:30:14 -05:00
parent 72a308801d
commit 81b590157e
5 changed files with 198 additions and 92 deletions

View File

@@ -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>

View File

@@ -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

View File

@@ -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>
); );
}; };

View File

@@ -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 <></>;
} }

View File

@@ -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">