feat: dark mode (#32)

This commit is contained in:
Ludvig Lundgren 2021-09-26 16:52:37 +02:00 committed by GitHub
parent 974ca95d80
commit 66048c5533
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 1736 additions and 1992 deletions

View file

@ -26,7 +26,7 @@
</script> </script>
<!-- {{end}} --> <!-- {{end}} -->
</head> </head>
<body class="bg-gray-100"> <body class="bg-gray-100 dark:bg-gray-900">
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <div id="root"></div>
</body> </body>

View file

@ -1,12 +1,11 @@
import React from "react"; import { QueryClient, QueryClientProvider } from "react-query";
import {QueryClient, QueryClientProvider} from "react-query"; import { BrowserRouter as Router, Route } from "react-router-dom";
import {BrowserRouter as Router, Route, Switch} from "react-router-dom";
import Login from "./screens/auth/login"; import Login from "./screens/auth/login";
import Logout from "./screens/auth/logout"; import Logout from "./screens/auth/logout";
import Base from "./screens/Base"; import Base from "./screens/Base";
import {ReactQueryDevtools} from "react-query/devtools"; import { ReactQueryDevtools } from "react-query/devtools";
import Layout from "./components/Layout"; import Layout from "./components/Layout";
import {baseUrl} from "./utils/utils"; import { baseUrl } from "./utils/utils";
function Protected() { function Protected() {
return ( return (
@ -22,11 +21,11 @@ function App() {
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<Router basename={baseUrl()}> <Router basename={baseUrl()}>
<Route exact={true} path="/login" component={Login}/> <Route exact={true} path="/login" component={Login} />
<Route exact={true} path="/logout" component={Logout}/> <Route exact={true} path="/logout" component={Logout} />
<Route exact={true} path="/*" component={Protected}/> <Route exact={true} path="/*" component={Protected} />
</Router> </Router>
<ReactQueryDevtools initialIsOpen={false}/> <ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider> </QueryClientProvider>
) )
}; };

View file

@ -1,5 +1,3 @@
import React from "react";
interface props { interface props {
text: string; text: string;
buttonText?: string; buttonText?: string;
@ -9,11 +7,11 @@ interface props {
export function EmptyListState({ text, buttonText, buttonOnClick }: props) { export function EmptyListState({ text, buttonText, buttonOnClick }: props) {
return ( return (
<div className="px-4 py-12 flex flex-col items-center"> <div className="px-4 py-12 flex flex-col items-center">
<p className="text-center text-gray-500">{text}</p> <p className="text-center text-gray-500 dark:text-white">{text}</p>
{buttonText && buttonOnClick && ( {buttonText && buttonOnClick && (
<button <button
type="button" type="button"
className="relative inline-flex items-center px-4 py-2 mt-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" className="relative inline-flex items-center px-4 py-2 mt-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
onClick={buttonOnClick} onClick={buttonOnClick}
> >
{buttonText} {buttonText}

View file

@ -199,7 +199,7 @@ function ListItem({ action, clients, filterID, idx }: ListItemProps) {
enabledMutation.mutate(action.id); enabledMutation.mutate(action.id);
}; };
useEffect(() => {}, [action]); useEffect(() => { }, [action]);
const cancelButtonRef = useRef(null); const cancelButtonRef = useRef(null);
@ -358,7 +358,7 @@ function ListItem({ action, clients, filterID, idx }: ListItemProps) {
onChange={toggleActive} onChange={toggleActive}
className={classNames( className={classNames(
action.enabled ? "bg-teal-500" : "bg-gray-200", action.enabled ? "bg-teal-500" : "bg-gray-200",
"z-10 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500" "z-10 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
)} )}
> >
<span className="sr-only">Use setting</span> <span className="sr-only">Use setting</span>
@ -469,7 +469,7 @@ function ListItem({ action, clients, filterID, idx }: ListItemProps) {
<div> <div>
<button <button
type="button" type="button"
className="bg-white border border-gray-300 rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500" className="bg-white border border-gray-300 rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
> >
Cancel Cancel
</button> </button>

View file

@ -1,5 +1,3 @@
import React from "react";
const DEBUG = ({ values }: any) => { const DEBUG = ({ values }: any) => {
if (process.env.NODE_ENV !== "development") { if (process.env.NODE_ENV !== "development") {
return null; return null;
@ -7,7 +5,7 @@ const DEBUG = ({ values }: any) => {
return ( return (
<div className="w-1/2 mx-auto mt-2 flex flex-col mt-12 mb-12"> <div className="w-1/2 mx-auto mt-2 flex flex-col mt-12 mb-12">
<pre className="mt-2">{JSON.stringify(values, 0 as any, 2)}</pre> <pre className="mt-2 dark:text-gray-500">{JSON.stringify(values, 0 as any, 2)}</pre>
</div> </div>
); );
}; };

View file

@ -1,4 +1,4 @@
import {PlusIcon} from "@heroicons/react/solid"; import { PlusIcon } from "@heroicons/react/solid";
interface props { interface props {
title: string; title: string;
@ -7,15 +7,15 @@ interface props {
buttonAction: any; buttonAction: any;
} }
const EmptySimple = ({ title, subtitle, buttonText, buttonAction}: props) => ( const EmptySimple = ({ title, subtitle, buttonText, buttonAction }: props) => (
<div className="text-center py-8"> <div className="text-center py-8">
<h3 className="mt-2 text-sm font-medium text-gray-900">{title}</h3> <h3 className="mt-2 text-sm font-medium text-gray-900 dark:text-white">{title}</h3>
<p className="mt-1 text-sm text-gray-500">{subtitle}</p> <p className="mt-1 text-sm text-gray-500 dark:text-gray-200">{subtitle}</p>
<div className="mt-6"> <div className="mt-6">
<button <button
type="button" type="button"
onClick={buttonAction} onClick={buttonAction}
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
> >
<PlusIcon className="-ml-1 mr-2 h-5 w-5" aria-hidden="true" /> <PlusIcon className="-ml-1 mr-2 h-5 w-5" aria-hidden="true" />
{buttonText} {buttonText}

View file

@ -7,8 +7,8 @@ interface Props {
const TitleSubtitle: React.FC<Props> = ({ title, subtitle }) => ( const TitleSubtitle: React.FC<Props> = ({ title, subtitle }) => (
<div> <div>
<h2 className="text-lg leading-6 font-medium text-gray-900">{title}</h2> <h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">{title}</h2>
<p className="mt-1 text-sm text-gray-500">{subtitle}</p> <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{subtitle}</p>
</div> </div>
) )

View file

@ -1,7 +1,7 @@
import { Field } from "react-final-form"; import { Field } from "react-final-form";
import React from "react"; import React from "react";
import Error from "./Error"; import Error from "./Error";
import {classNames} from "../../styles/utils"; import { classNames } from "../../styles/utils";
type COL_WIDTHS = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; type COL_WIDTHS = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
@ -14,30 +14,30 @@ interface Props {
autoComplete?: string; autoComplete?: string;
} }
const PasswordField: React.FC<Props> = ({ name, label, placeholder, columns , className, autoComplete}) => ( const PasswordField: React.FC<Props> = ({ name, label, placeholder, columns, className, autoComplete }) => (
<div <div
className={classNames( className={classNames(
columns ? `col-span-${columns}` : "col-span-12" columns ? `col-span-${columns}` : "col-span-12"
)} )}
> >
{label && ( {label && (
<label htmlFor={name} className="block text-xs font-bold text-gray-700 uppercase tracking-wide"> <label htmlFor={name} className="block text-xs font-bold text-gray-700 dark:text-white uppercase tracking-wide">
{label} {label}
</label> </label>
)} )}
<Field <Field
name={name} name={name}
render={({input, meta}) => ( render={({ input }) => (
<input <input
{...input} {...input}
id={name} id={name}
type="password" type="password"
autoComplete={autoComplete} autoComplete={autoComplete}
className="mt-2 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-light-blue-500 focus:border-light-blue-500 sm:text-sm" className="mt-2 block w-full border border-gray-300 dark:border-gray-700 dark:bg-gray-800 dark:text-white rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
placeholder={placeholder} placeholder={placeholder}
/> />
)} )}
/> />
<div> <div>
<Error name={name} classNames="text-red mt-2" /> <Error name={name} classNames="text-red mt-2" />
</div> </div>

View file

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import {Switch} from "@headlessui/react"; import { Switch } from "@headlessui/react";
import {Field} from "react-final-form"; import { Field } from "react-final-form";
import {classNames} from "../../styles/utils"; import { classNames } from "../../styles/utils";
interface Props { interface Props {
name: string; name: string;
@ -11,32 +11,32 @@ interface Props {
className?: string; className?: string;
} }
const SwitchGroup: React.FC<Props> = ({name, label, description, defaultValue}) => ( const SwitchGroup: React.FC<Props> = ({ name, label, description, defaultValue }) => (
<ul className="mt-2 divide-y divide-gray-200"> <ul className="mt-2 divide-y divide-gray-200 dark:divide-gray-700">
<Switch.Group as="li" className="py-4 flex items-center justify-between"> <Switch.Group as="li" className="py-4 flex items-center justify-between">
<div className="flex flex-col"> <div className="flex flex-col">
<Switch.Label as="p" className="text-sm font-medium text-gray-900" <Switch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-white"
passive> passive>
{label} {label}
</Switch.Label> </Switch.Label>
{description && ( {description && (
<Switch.Description className="text-sm text-gray-500"> <Switch.Description className="text-sm text-gray-500 dark:text-gray-700">
{description} {description}
</Switch.Description> </Switch.Description>
)} )}
</div> </div>
<Field <Field
name={name} name={name}
defaultValue={defaultValue as any} defaultValue={defaultValue as any}
render={({input: {onChange, checked, value}}) => ( render={({ input: { onChange, checked, value } }) => (
<Switch <Switch
value={value} value={value}
checked={value} checked={value}
onChange={onChange} onChange={onChange}
className={classNames( className={classNames(
value ? 'bg-teal-500' : 'bg-gray-200', value ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-500',
'ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500' 'ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
)} )}
> >
<span className="sr-only">Use setting</span> <span className="sr-only">Use setting</span>

View file

@ -1,7 +1,7 @@
import { Field } from "react-final-form";
import React from "react"; import React from "react";
import { Field } from "react-final-form";
import Error from "./Error"; import Error from "./Error";
import {classNames} from "../../styles/utils"; import { classNames } from "../../styles/utils";
type COL_WIDTHS = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; type COL_WIDTHS = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
@ -14,31 +14,31 @@ interface Props {
autoComplete?: string; autoComplete?: string;
} }
const TextField: React.FC<Props> = ({ name, label, placeholder, columns , className, autoComplete}) => ( const TextField: React.FC<Props> = ({ name, label, placeholder, columns, className, autoComplete }) => (
<div <div
className={classNames( className={classNames(
columns ? `col-span-${columns}` : "col-span-12" columns ? `col-span-${columns}` : "col-span-12"
)} )}
> >
{label && ( {label && (
<label htmlFor={name} className="block text-xs font-bold text-gray-700 uppercase tracking-wide"> <label htmlFor={name} className="block text-xs font-bold text-gray-700 dark:text-white uppercase tracking-wide">
{label} {label}
</label> </label>
)} )}
<Field <Field
name={name} name={name}
render={({input, meta}) => ( render={({ input }) => (
<input <input
{...input} {...input}
id={name} id={name}
type="text" type="text"
value={input.value} value={input.value}
autoComplete={autoComplete} autoComplete={autoComplete}
className="mt-2 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-light-blue-500 focus:border-light-blue-500 sm:text-sm" className="mt-2 block w-full border border-gray-300 dark:border-gray-700 dark:bg-gray-800 dark:text-white rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
placeholder={placeholder} placeholder={placeholder}
/> />
)} )}
/> />
<div> <div>
<Error name={name} classNames="text-red mt-2" /> <Error name={name} classNames="text-red mt-2" />
</div> </div>

View file

@ -1,5 +1,5 @@
import { Field } from "react-final-form";
import React from "react"; import React from "react";
import { Field } from "react-final-form";
import Error from "./Error"; import Error from "./Error";
import { classNames } from "../../styles/utils"; import { classNames } from "../../styles/utils";
@ -14,12 +14,12 @@ interface Props {
hidden?: boolean; hidden?: boolean;
} }
const TextFieldWide: React.FC<Props> = ({ name, label, help, placeholder, defaultValue, required, hidden, className}) => ( const TextFieldWide: React.FC<Props> = ({ name, label, help, placeholder, defaultValue, required, hidden, className }) => (
<div hidden={hidden} <div hidden={hidden}
className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5"> className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
<div> <div>
<label htmlFor={name} className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"> <label htmlFor={name} className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2">
{label} {required && <span className="text-gray-500">*</span>} {label} {required && <span className="text-gray-500">*</span>}
</label> </label>
</div> </div>
@ -32,7 +32,7 @@ const TextFieldWide: React.FC<Props> = ({ name, label, help, placeholder, defaul
{...input} {...input}
id={name} id={name}
type="text" type="text"
className={classNames(meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 focus:border-indigo-500 border-gray-300", "block w-full shadow-sm sm:text-sm rounded-md")} className={classNames(meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700", "block w-full shadow-sm dark:bg-gray-800 sm:text-sm dark:text-white rounded-md")}
placeholder={placeholder} placeholder={placeholder}
hidden={hidden} hidden={hidden}
/> />

View file

@ -1,5 +1,5 @@
import { Field } from "react-final-form";
import React from "react"; import React from "react";
import { Field } from "react-final-form";
import Error from "../Error"; import Error from "../Error";
import { classNames } from "../../../styles/utils"; import { classNames } from "../../../styles/utils";
@ -28,7 +28,7 @@ const NumberFieldWide: React.FC<Props> = ({
<div> <div>
<label <label
htmlFor={name} htmlFor={name}
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2" className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2"
> >
{label} {required && <span className="text-gray-500">*</span>} {label} {required && <span className="text-gray-500">*</span>}
</label> </label>
@ -46,15 +46,15 @@ const NumberFieldWide: React.FC<Props> = ({
className={classNames( className={classNames(
meta.touched && meta.error meta.touched && meta.error
? "focus:ring-red-500 focus:border-red-500 border-red-500" ? "focus:ring-red-500 focus:border-red-500 border-red-500"
: "focus:ring-indigo-500 focus:border-indigo-500 border-gray-300", : "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700",
"block w-full shadow-sm sm:text-sm rounded-md" "block w-full shadow-sm dark:bg-gray-800 sm:text-sm dark:text-white rounded-md"
)} )}
placeholder={placeholder} placeholder={placeholder}
/> />
)} )}
/> />
{help && ( {help && (
<p className="mt-2 text-sm text-gray-500" id={`${name}-description`}>{help}</p> <p className="mt-2 text-sm text-gray-500 dark:text-gray-200" id={`${name}-description`}>{help}</p>
)} )}
<Error name={name} classNames="block text-red-500 mt-2" /> <Error name={name} classNames="block text-red-500 mt-2" />
</div> </div>

View file

@ -21,7 +21,7 @@ function PasswordField({ name, label, placeholder, defaultValue, help, required
className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5"> className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
<div> <div>
<label htmlFor={name} className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"> <label htmlFor={name} className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2">
{label} {required && <span className="text-gray-500">*</span>} {label} {required && <span className="text-gray-500">*</span>}
</label> </label>
</div> </div>
@ -35,7 +35,7 @@ function PasswordField({ name, label, placeholder, defaultValue, help, required
{...input} {...input}
id={name} id={name}
type={isVisible ? "text" : "password"} type={isVisible ? "text" : "password"}
className={classNames(meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 focus:border-indigo-500 border-gray-300", "block w-full shadow-sm sm:text-sm rounded-md")} className={classNames(meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700", "block w-full dark:bg-gray-800 shadow-sm dark:text-gray-100 sm:text-sm rounded-md")}
placeholder={placeholder} placeholder={placeholder}
/> />
<div className="absolute inset-y-0 right-0 px-3 flex items-center" onClick={toggleVisibility}> <div className="absolute inset-y-0 right-0 px-3 flex items-center" onClick={toggleVisibility}>

View file

@ -17,7 +17,7 @@ function RadioFieldsetWide({ name, legend, options }: props) {
<fieldset> <fieldset>
<div className="space-y-2 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:px-6 sm:py-5"> <div className="space-y-2 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:px-6 sm:py-5">
<div> <div>
<legend className="text-sm font-medium text-gray-900"> <legend className="text-sm font-medium text-gray-900 dark:text-white">
{legend} {legend}
</legend> </legend>
</div> </div>
@ -31,7 +31,7 @@ function RadioFieldsetWide({ name, legend, options }: props) {
<RadioGroup.Label className="sr-only"> <RadioGroup.Label className="sr-only">
Privacy setting Privacy setting
</RadioGroup.Label> </RadioGroup.Label>
<div className="bg-white rounded-md -space-y-px"> <div className="bg-white dark:bg-gray-800 rounded-md -space-y-px">
{options.map((setting, settingIdx) => ( {options.map((setting, settingIdx) => (
<RadioGroup.Option <RadioGroup.Option
key={setting.value} key={setting.value}
@ -45,8 +45,8 @@ function RadioFieldsetWide({ name, legend, options }: props) {
? "rounded-bl-md rounded-br-md" ? "rounded-bl-md rounded-br-md"
: "", : "",
checked checked
? "bg-indigo-50 border-indigo-200 z-10" ? "bg-indigo-50 dark:bg-gray-700 border-indigo-200 dark:border-blue-600 z-10"
: "border-gray-200", : "border-gray-200 dark:border-gray-700",
"relative border p-4 flex cursor-pointer focus:outline-none" "relative border p-4 flex cursor-pointer focus:outline-none"
) )
} }
@ -56,10 +56,10 @@ function RadioFieldsetWide({ name, legend, options }: props) {
<span <span
className={classNames( className={classNames(
checked checked
? "bg-indigo-600 border-transparent" ? "bg-indigo-600 dark:bg-blue-600 border-transparent"
: "bg-white border-gray-300", : "bg-white border-gray-300 dark:border-gray-300",
active active
? "ring-2 ring-offset-2 ring-indigo-500" ? "ring-2 ring-offset-2 ring-indigo-500 dark:ring-blue-500"
: "", : "",
"h-4 w-4 mt-0.5 cursor-pointer rounded-full border flex items-center justify-center" "h-4 w-4 mt-0.5 cursor-pointer rounded-full border flex items-center justify-center"
)} )}
@ -71,7 +71,7 @@ function RadioFieldsetWide({ name, legend, options }: props) {
<RadioGroup.Label <RadioGroup.Label
as="span" as="span"
className={classNames( className={classNames(
checked ? "text-indigo-900" : "text-gray-900", checked ? "text-indigo-900 dark:text-blue-500" : "text-gray-900 dark:text-gray-300",
"block text-sm font-medium" "block text-sm font-medium"
)} )}
> >
@ -80,7 +80,7 @@ function RadioFieldsetWide({ name, legend, options }: props) {
<RadioGroup.Description <RadioGroup.Description
as="span" as="span"
className={classNames( className={classNames(
checked ? "text-indigo-700" : "text-gray-500", checked ? "text-indigo-700 dark:text-blue-500" : "text-gray-500",
"block text-sm" "block text-sm"
)} )}
> >

View file

@ -1,6 +1,6 @@
import {Fragment} from "react"; import { Fragment } from "react";
import {Dialog, Transition} from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import {ExclamationIcon} from "@heroicons/react/solid"; import { ExclamationIcon } from "@heroicons/react/solid";
interface props { interface props {
isOpen: boolean; isOpen: boolean;
@ -34,10 +34,9 @@ const DeleteModal = ({ isOpen, buttonRef, toggle, deleteAction, title, text }: p
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" /> <Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child> </Transition.Child>
{/* This element is to trick the browser into centering the modal contents. */}
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true"> <span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203; &#8203;
</span> </span>
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}
enter="ease-out duration-300" enter="ease-out duration-300"
@ -48,24 +47,24 @@ const DeleteModal = ({ isOpen, buttonRef, toggle, deleteAction, title, text }: p
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
> >
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"> <div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"> <div className="bg-white dark:bg-gray-700 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start"> <div className="sm:flex sm:items-start">
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10"> <div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 dark:bg-red-400 sm:mx-0 sm:h-10 sm:w-10">
<ExclamationIcon className="h-6 w-6 text-red-600" aria-hidden="true" /> <ExclamationIcon className="h-6 w-6 text-red-600 dark:text-red-600" aria-hidden="true" />
</div> </div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"> <div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<Dialog.Title as="h3" className="text-lg leading-6 font-medium text-gray-900"> <Dialog.Title as="h3" className="text-lg leading-6 font-medium text-gray-900 dark:text-white">
{title} {title}
</Dialog.Title> </Dialog.Title>
<div className="mt-2"> <div className="mt-2">
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500 dark:text-gray-300">
{text} {text}
</p> </p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse"> <div className="bg-gray-50 dark:bg-gray-800 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button <button
type="button" type="button"
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm" className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm"
@ -75,7 +74,7 @@ const DeleteModal = ({ isOpen, buttonRef, toggle, deleteAction, title, text }: p
</button> </button>
<button <button
type="button" type="button"
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm" className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 dark:border-gray-600 shadow-sm px-4 py-2 bg-white dark:bg-gray-700 text-base font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
onClick={toggle} onClick={toggle}
ref={buttonRef} ref={buttonRef}
> >

View file

@ -1,6 +1,7 @@
import { FC } from 'react' import { FC } from 'react'
import { XIcon, CheckCircleIcon, ExclamationIcon, ExclamationCircleIcon } from '@heroicons/react/solid' import { XIcon, CheckCircleIcon, ExclamationIcon, ExclamationCircleIcon } from '@heroicons/react/solid'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import { classNames } from '../../styles/utils'
type Props = { type Props = {
type: 'error' | 'success' | 'warning' type: 'error' | 'success' | 'warning'
@ -14,9 +15,9 @@ const Toast: FC<Props> = ({
t t
}) => { }) => {
return ( return (
<div className={`${ <div className={classNames(
t.visible ? 'animate-enter' : 'animate-leave' t.visible ? 'animate-enter' : 'animate-leave',
} max-w-sm w-full bg-white shadow-lg rounded-lg pointer-events-auto ring-1 ring-black ring-opacity-5 overflow-hidden transition-all`}> "max-w-sm w-full bg-white dark:bg-gray-800 shadow-lg rounded-lg pointer-events-auto ring-1 ring-black ring-opacity-5 overflow-hidden transition-all")}>
<div className="p-4"> <div className="p-4">
<div className="flex items-start"> <div className="flex items-start">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
@ -25,16 +26,16 @@ const Toast: FC<Props> = ({
{type === 'warning' && <ExclamationIcon className="h-6 w-6 text-yellow-400" aria-hidden="true" />} {type === 'warning' && <ExclamationIcon className="h-6 w-6 text-yellow-400" aria-hidden="true" />}
</div> </div>
<div className="ml-3 w-0 flex-1 pt-0.5"> <div className="ml-3 w-0 flex-1 pt-0.5">
<p className="text-sm font-medium text-gray-900"> <p className="text-sm font-medium text-gray-900 dark:text-gray-200">
{type === 'success' && "Success"} {type === 'success' && "Success"}
{type === 'error' && "Error"} {type === 'error' && "Error"}
{type === 'warning' && "Warning"} {type === 'warning' && "Warning"}
</p> </p>
<p className="mt-1 text-sm text-gray-500">{body}</p> <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{body}</p>
</div> </div>
<div className="ml-4 flex-shrink-0 flex"> <div className="ml-4 flex-shrink-0 flex">
<button <button
className="bg-white rounded-md inline-flex text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" className="bg-white dark:bg-gray-700 rounded-md inline-flex text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
onClick={() => { onClick={() => {
toast.dismiss(t.id) toast.dismiss(t.id)
}} }}

View file

@ -0,0 +1,140 @@
import { Fragment, useRef } from "react";
import { XIcon } from "@heroicons/react/solid";
import { Dialog, Transition } from "@headlessui/react";
import { Form } from "react-final-form";
import DEBUG from "../../components/debug";
import { useToggle } from "../../hooks/hooks";
import { DeleteModal } from "../../components/modals";
import { classNames } from "../../styles/utils";
interface props {
title: string;
initialValues: any;
mutators?: any;
validate?: any;
onSubmit: any;
isOpen: boolean;
toggle: any;
children?: (values: any) => React.ReactNode;
deleteAction?: any
type: "CREATE" | "UPDATE";
}
function SlideOver({ title, initialValues, mutators, validate, onSubmit, deleteAction, isOpen, toggle, type, children }: props) {
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false)
const cancelModalButtonRef = useRef(null)
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" static className="fixed inset-0 overflow-hidden" open={isOpen} onClose={toggle}>
{deleteAction && (
<DeleteModal
isOpen={deleteModalIsOpen}
toggle={toggleDeleteModal}
buttonRef={cancelModalButtonRef}
deleteAction={deleteAction}
title={`Remove ${title}`}
text={`Are you sure you want to remove this ${title}? This action cannot be undone.`}
/>
)}
<div className="absolute inset-0 overflow-hidden">
<Dialog.Overlay className="absolute inset-0" />
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
<Transition.Child
as={Fragment}
enter="transform transition ease-in-out duration-500 sm:duration-700"
enterFrom="translate-x-full"
enterTo="translate-x-0"
leave="transform transition ease-in-out duration-500 sm:duration-700"
leaveFrom="translate-x-0"
leaveTo="translate-x-full"
>
<div className="w-screen max-w-2xl dark:border-gray-700 border-l">
<Form
initialValues={initialValues}
mutators={mutators}
onSubmit={onSubmit}
validate={validate}
>
{({ handleSubmit, values }) => {
return (
<form className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll"
onSubmit={handleSubmit}>
<div className="flex-1">
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
<div className="flex items-start justify-between space-x-3">
<div className="space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">{type === "CREATE" ? "Create" : "Update"} {title}</Dialog.Title>
<p className="text-sm text-gray-500 dark:text-gray-400">
{type === "CREATE" ? "Create" : "Update"} {title}.
</p>
</div>
<div className="h-7 flex items-center">
<button
type="button"
className="bg-white dark:bg-gray-900 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
onClick={toggle}
>
<span className="sr-only">Close panel</span>
<XIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
</div>
</div>
{children !== undefined && children(values)}
</div>
<div className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6">
<div className={classNames(type === "CREATE" ? "justify-end" : "justify-between", "space-x-3 flex")}>
{type === "UPDATE" && (
<button
type="button"
className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 dark:text-red-900 bg-red-100 dark:bg-red-500 hover:bg-red-200 dark:hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
onClick={toggleDeleteModal}
>
Remove
</button>
)}
<div>
<button
type="button"
className="bg-white dark:bg-gray-700 py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
onClick={toggle}
>
Cancel
</button>
<button
type="submit"
className="ml-4 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
{type === "CREATE" ? "Create" : "Save"}
</button>
</div>
</div>
</div>
<DEBUG values={values} />
</form>
)
}}
</Form>
</div>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
)
}
export default SlideOver;

View file

@ -0,0 +1 @@
export { default as SlideOver } from "./SlideOver";

View file

@ -1,20 +1,17 @@
import React, {Fragment, useEffect} from "react"; import { Fragment, useEffect } from "react";
import {useMutation} from "react-query"; import { useMutation } from "react-query";
import {Filter} from "../../domain/interfaces"; import { Filter } from "../../domain/interfaces";
import {queryClient} from "../../App"; import { queryClient } from "../../App";
import {XIcon} from "@heroicons/react/solid"; import { XIcon } from "@heroicons/react/solid";
import {Dialog, Transition} from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import {Field, Form} from "react-final-form"; import { Field, Form } from "react-final-form";
import DEBUG from "../../components/debug"; import DEBUG from "../../components/debug";
import APIClient from "../../api/APIClient"; import APIClient from "../../api/APIClient";
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import Toast from '../../components/notifications/Toast'; import Toast from '../../components/notifications/Toast';
function FilterAddForm({ isOpen, toggle }: any) {
const required = (value: any) => (value ? undefined : 'Required')
function FilterAddForm({isOpen, toggle}: any) {
const mutation = useMutation((filter: Filter) => APIClient.filters.create(filter), { const mutation = useMutation((filter: Filter) => APIClient.filters.create(filter), {
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries('filter'); queryClient.invalidateQueries('filter');
@ -32,11 +29,21 @@ function FilterAddForm({isOpen, toggle}: any) {
mutation.mutate(data) mutation.mutate(data)
} }
const validate = (values: any) => {
const errors = {} as any;
if (!values.name) {
errors.name = "Required";
}
return errors;
}
return ( return (
<Transition.Root show={isOpen} as={Fragment}> <Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" static className="fixed inset-0 overflow-hidden" open={isOpen} onClose={toggle}> <Dialog as="div" static className="fixed inset-0 overflow-hidden" open={isOpen} onClose={toggle}>
<div className="absolute inset-0 overflow-hidden"> <div className="absolute inset-0 overflow-hidden">
<Dialog.Overlay className="absolute inset-0"/> <Dialog.Overlay className="absolute inset-0" />
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16"> <div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
<Transition.Child <Transition.Child
@ -48,7 +55,7 @@ function FilterAddForm({isOpen, toggle}: any) {
leaveFrom="translate-x-0" leaveFrom="translate-x-0"
leaveTo="translate-x-full" leaveTo="translate-x-full"
> >
<div className="w-screen max-w-2xl"> <div className="w-screen max-w-2xl border-l dark:border-gray-700">
<Form <Form
initialValues={{ initialValues={{
@ -59,38 +66,34 @@ function FilterAddForm({isOpen, toggle}: any) {
sources: [], sources: [],
containers: [] containers: []
}} }}
// validate={validate} validate={validate}
onSubmit={onSubmit} onSubmit={onSubmit}
> >
{({handleSubmit, values}) => { {({ handleSubmit, values }) => {
return ( return (
<form className="h-full flex flex-col bg-white shadow-xl overflow-y-scroll" onSubmit={handleSubmit}> <form className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll" onSubmit={handleSubmit}>
<div className="flex-1"> <div className="flex-1">
{/* Header */} <div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
<div className="px-4 py-6 bg-gray-50 sm:px-6">
<div className="flex items-start justify-between space-x-3"> <div className="flex items-start justify-between space-x-3">
<div className="space-y-1"> <div className="space-y-1">
<Dialog.Title <Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Create filter</Dialog.Title>
className="text-lg font-medium text-gray-900">Create <p className="text-sm text-gray-500 dark:text-gray-400">
filter</Dialog.Title>
<p className="text-sm text-gray-500">
Add new filter. Add new filter.
</p> </p>
</div> </div>
<div className="h-7 flex items-center"> <div className="h-7 flex items-center">
<button <button
type="button" type="button"
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500" className="light:bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
onClick={toggle} onClick={toggle}
> >
<span className="sr-only">Close panel</span> <span className="sr-only">Close panel</span>
<XIcon className="h-6 w-6" aria-hidden="true"/> <XIcon className="h-6 w-6" aria-hidden="true" />
</button> </button>
</div> </div>
</div> </div>
</div> </div>
{/* Divider container */}
<div <div
className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200"> className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<div <div
@ -98,21 +101,21 @@ function FilterAddForm({isOpen, toggle}: any) {
<div> <div>
<label <label
htmlFor="name" htmlFor="name"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2" className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2"
> >
Name Name
</label> </label>
</div> </div>
<Field name="name" validate={required}> <Field name="name">
{({input, meta}) => ( {({ input, meta }) => (
<div className="sm:col-span-2"> <div className="sm:col-span-2">
<input <input
type="text" type="text"
{...input} {...input}
className="block w-full shadow-sm sm:text-sm focus:ring-indigo-500 focus:border-indigo-500 border-gray-300 rounded-md" className="block w-full shadow-sm dark:bg-gray-800 border-gray-300 dark:border-gray-700 sm:text-sm dark:text-white focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 rounded-md"
/> />
{meta.touched && meta.error && {meta.touched && meta.error &&
<span>{meta.error}</span>} <span className="block mt-2 text-red-500">{meta.error}</span>}
</div> </div>
)} )}
</Field> </Field>
@ -122,24 +125,24 @@ function FilterAddForm({isOpen, toggle}: any) {
</div> </div>
<div <div
className="flex-shrink-0 px-4 border-t border-gray-200 py-5 sm:px-6"> className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6">
<div className="space-x-3 flex justify-end"> <div className="space-x-3 flex justify-end">
<button <button
type="button" type="button"
className="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" className="bg-white dark:bg-gray-800 py-2 px-4 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
onClick={toggle} onClick={toggle}
> >
Cancel Cancel
</button> </button>
<button <button
type="submit" type="submit"
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
> >
Create Create
</button> </button>
</div> </div>
</div> </div>
<DEBUG values={values}/> <DEBUG values={values} />
</form> </form>
) )
}} }}

View file

@ -2,10 +2,6 @@ export { default as FilterAddForm } from "./filters/FilterAddForm";
export { default as FilterActionAddForm } from "./filters/FilterActionAddForm"; export { default as FilterActionAddForm } from "./filters/FilterActionAddForm";
export { default as FilterActionUpdateForm } from "./filters/FilterActionUpdateForm"; export { default as FilterActionUpdateForm } from "./filters/FilterActionUpdateForm";
export { default as DownloadClientAddForm } from "./settings/downloadclient/DownloadClientAddForm"; export { DownloadClientAddForm, DownloadClientUpdateForm } from "./settings/DownloadClientForms";
export { default as DownloadClientUpdateForm } from "./settings/downloadclient/DownloadClientUpdateForm"; export { IndexerAddForm, IndexerUpdateForm } from "./settings/IndexerForms";
export { IrcNetworkAddForm, IrcNetworkUpdateForm } from "./settings/IrcForms";
export { default as IndexerAddForm } from "./settings/IndexerAddForm";
export { default as IndexerUpdateForm } from "./settings/IndexerUpdateForm";
export { default as IrcNetworkAddForm } from "./settings/IrcNetworkAddForm";

View file

@ -0,0 +1,617 @@
import { Fragment, useRef, useState } from "react";
import { useMutation } from "react-query";
import {
DOWNLOAD_CLIENT_TYPES,
DownloadClient,
} from "../../domain/interfaces";
import { Dialog, Transition } from "@headlessui/react";
import { XIcon } from "@heroicons/react/solid";
import { classNames } from "../../styles/utils";
import { Form, useField } from "react-final-form";
import DEBUG from "../../components/debug";
import { SwitchGroup, TextFieldWide } from "../../components/inputs";
import { queryClient } from "../../App";
import APIClient from "../../api/APIClient";
import { sleep } from "../../utils/utils";
import { DownloadClientTypeOptions } from "../../domain/constants";
import { NumberFieldWide, PasswordFieldWide, RadioFieldsetWide } from "../../components/inputs/wide";
import { toast } from 'react-hot-toast'
import Toast from '../../components/notifications/Toast';
import { useToggle } from "../../hooks/hooks";
import { DeleteModal } from "../../components/modals";
function FormFieldsDefault() {
return (
<Fragment>
<TextFieldWide name="host" label="Host" help="Url domain.ltd/client" />
<NumberFieldWide name="port" label="Port" />
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="ssl" label="SSL" />
</div>
<TextFieldWide name="username" label="Username" />
<PasswordFieldWide name="password" label="Password" />
</Fragment>
);
}
function FormFieldsArr() {
const { input } = useField("settings.basic.auth");
return (
<Fragment>
<TextFieldWide name="host" label="Host" help="Full url like http(s)://domain.ltd/" />
<PasswordFieldWide name="settings.apikey" label="API key" />
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="settings.basic.auth" label="Basic auth" />
</div>
{input.value === true && (
<Fragment>
<TextFieldWide name="settings.basic.username" label="Username" />
<PasswordFieldWide name="settings.basic.password" label="Password" />
</Fragment>
)}
</Fragment>
);
}
export const componentMap: any = {
DELUGE_V1: <FormFieldsDefault />,
DELUGE_V2: <FormFieldsDefault />,
QBITTORRENT: <FormFieldsDefault />,
RADARR: <FormFieldsArr />,
SONARR: <FormFieldsArr />,
LIDARR: <FormFieldsArr />,
};
function FormFieldsRulesBasic() {
const { input: enabled } = useField("settings.rules.enabled");
return (
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
<div className="px-6 space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Rules</Dialog.Title>
<p className="text-sm text-gray-500 dark:text-gray-400">
Manage max downloads.
</p>
</div>
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="settings.rules.enabled" label="Enabled" />
</div>
{enabled.value === true && (
<Fragment>
<NumberFieldWide name="settings.rules.max_active_downloads" label="Max active downloads" />
</Fragment>
)}
</div>
);
}
function FormFieldsRules() {
const { input } = useField("settings.rules.ignore_slow_torrents");
const { input: enabled } = useField("settings.rules.enabled");
return (
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
<div className="px-6 space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Rules</Dialog.Title>
<p className="text-sm text-gray-500 dark:text-gray-400">
Manage max downloads etc.
</p>
</div>
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="settings.rules.enabled" label="Enabled" />
</div>
{enabled.value === true && (
<Fragment>
<NumberFieldWide name="settings.rules.max_active_downloads" label="Max active downloads" />
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="settings.rules.ignore_slow_torrents" label="Ignore slow torrents" />
</div>
{input.value === true && (
<Fragment>
<NumberFieldWide name="settings.rules.download_speed_threshold" label="Download speed threshold" placeholder="in KB/s" help="If download speed is below this when max active downloads is hit, download anyways. KB/s" />
</Fragment>
)}
</Fragment>
)}
</div>
);
}
export const rulesComponentMap: any = {
DELUGE_V1: <FormFieldsRulesBasic />,
DELUGE_V2: <FormFieldsRulesBasic />,
QBITTORRENT: <FormFieldsRules />,
};
export function DownloadClientAddForm({ isOpen, toggle }: any) {
const [isTesting, setIsTesting] = useState(false);
const [isSuccessfulTest, setIsSuccessfulTest] = useState(false);
const [isErrorTest, setIsErrorTest] = useState(false);
const mutation = useMutation(
(client: DownloadClient) => APIClient.download_clients.create(client),
{
onSuccess: () => {
queryClient.invalidateQueries(["downloadClients"]);
toast.custom((t) => <Toast type="success" body="Client was added" t={t} />)
toggle();
},
onError: () => {
toast.custom((t) => <Toast type="error" body="Client could not be added" t={t} />)
}
}
);
const testClientMutation = useMutation(
(client: DownloadClient) => APIClient.download_clients.test(client),
{
onMutate: () => {
setIsTesting(true);
setIsErrorTest(false);
setIsSuccessfulTest(false);
},
onSuccess: () => {
sleep(1000)
.then(() => {
setIsTesting(false);
setIsSuccessfulTest(true);
})
.then(() => {
sleep(2500).then(() => {
setIsSuccessfulTest(false);
});
});
},
onError: (error) => {
console.log('not added')
setIsTesting(false);
setIsErrorTest(true);
sleep(2500).then(() => {
setIsErrorTest(false);
});
},
}
);
const onSubmit = (data: any) => {
mutation.mutate(data);
};
const testClient = (data: any) => {
testClientMutation.mutate(data);
};
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog
as="div"
static
className="fixed inset-0 overflow-hidden"
open={isOpen}
onClose={toggle}
>
<div className="absolute inset-0 overflow-hidden">
<Dialog.Overlay className="absolute inset-0" />
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
<Transition.Child
as={Fragment}
enter="transform transition ease-in-out duration-500 sm:duration-700"
enterFrom="translate-x-full"
enterTo="translate-x-0"
leave="transform transition ease-in-out duration-500 sm:duration-700"
leaveFrom="translate-x-0"
leaveTo="translate-x-full"
>
<div className="w-screen max-w-2xl border-l dark:border-gray-700">
<Form
initialValues={{
name: "",
type: DOWNLOAD_CLIENT_TYPES.qBittorrent,
enabled: true,
host: "",
port: 10000,
ssl: false,
username: "",
password: "",
}}
onSubmit={onSubmit}
>
{({ handleSubmit, values }) => {
return (
<form
className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll"
onSubmit={handleSubmit}
>
<div className="flex-1">
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
<div className="flex items-start justify-between space-x-3">
<div className="space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">
Add client
</Dialog.Title>
<p className="text-sm text-gray-500 dark:text-gray-400">
Add download client.
</p>
</div>
<div className="h-7 flex items-center">
<button
type="button"
className="bg-white dark:bg-gray-800 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
onClick={toggle}
>
<span className="sr-only">Close panel</span>
<XIcon
className="h-6 w-6"
aria-hidden="true"
/>
</button>
</div>
</div>
</div>
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700">
<TextFieldWide name="name" label="Name" />
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200 dark:divide-gray-700">
<SwitchGroup name="enabled" label="Enabled" />
</div>
<RadioFieldsetWide
name="type"
legend="Type"
options={DownloadClientTypeOptions}
/>
<div>{componentMap[values.type]}</div>
</div>
</div>
{rulesComponentMap[values.type]}
<div className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6">
<div className="space-x-3 flex justify-end">
<button
type="button"
className={classNames(
isSuccessfulTest
? "text-green-500 border-green-500 bg-green-50"
: isErrorTest
? "text-red-500 border-red-500 bg-red-50"
: "border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-400 bg-white dark:bg-gray-700 hover:bg-gray-50 focus:border-rose-700 active:bg-rose-700",
isTesting ? "cursor-not-allowed" : "",
"mr-2 inline-flex items-center px-4 py-2 border font-medium rounded-md shadow-sm text-sm transition ease-in-out duration-150 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
)}
disabled={isTesting}
onClick={() => testClient(values)}
>
{isTesting ? (
<svg
className="animate-spin h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
) : isSuccessfulTest ? (
"OK!"
) : isErrorTest ? (
"ERROR"
) : (
"Test"
)}
</button>
<button
type="button"
className="bg-white dark:bg-gray-700 py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
onClick={toggle}
>
Cancel
</button>
<button
type="submit"
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
>
Create
</button>
</div>
</div>
<DEBUG values={values} />
</form>
);
}}
</Form>
</div>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
}
export function DownloadClientUpdateForm({ client, isOpen, toggle }: any) {
const [isTesting, setIsTesting] = useState(false);
const [isSuccessfulTest, setIsSuccessfulTest] = useState(false);
const [isErrorTest, setIsErrorTest] = useState(false);
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
const mutation = useMutation(
(client: DownloadClient) => APIClient.download_clients.update(client),
{
onSuccess: () => {
queryClient.invalidateQueries(["downloadClients"]);
toast.custom((t) => <Toast type="success" body={`${client.name} was updated successfully`} t={t} />)
toggle();
},
}
);
const deleteMutation = useMutation(
(clientID: number) => APIClient.download_clients.delete(clientID),
{
onSuccess: () => {
queryClient.invalidateQueries();
toast.custom((t) => <Toast type="success" body={`${client.name} was deleted.`} t={t} />)
toggleDeleteModal();
},
}
);
const testClientMutation = useMutation(
(client: DownloadClient) => APIClient.download_clients.test(client),
{
onMutate: () => {
setIsTesting(true);
setIsErrorTest(false);
setIsSuccessfulTest(false);
},
onSuccess: () => {
sleep(1000)
.then(() => {
setIsTesting(false);
setIsSuccessfulTest(true);
})
.then(() => {
sleep(2500).then(() => {
setIsSuccessfulTest(false);
});
});
},
onError: (error) => {
setIsTesting(false);
setIsErrorTest(true);
sleep(2500).then(() => {
setIsErrorTest(false);
});
},
}
);
const onSubmit = (data: any) => {
mutation.mutate(data);
};
const cancelButtonRef = useRef(null);
const cancelModalButtonRef = useRef(null);
const deleteAction = () => {
deleteMutation.mutate(client.id);
};
const testClient = (data: any) => {
testClientMutation.mutate(data);
};
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog
as="div"
static
className="fixed inset-0 overflow-hidden"
open={isOpen}
onClose={toggle}
initialFocus={cancelButtonRef}
>
<DeleteModal
isOpen={deleteModalIsOpen}
toggle={toggleDeleteModal}
buttonRef={cancelModalButtonRef}
deleteAction={deleteAction}
title="Remove download client"
text="Are you sure you want to remove this download client? This action cannot be undone."
/>
<div className="absolute inset-0 overflow-hidden">
<Dialog.Overlay className="absolute inset-0" />
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
<Transition.Child
as={Fragment}
enter="transform transition ease-in-out duration-500 sm:duration-700"
enterFrom="translate-x-full"
enterTo="translate-x-0"
leave="transform transition ease-in-out duration-500 sm:duration-700"
leaveFrom="translate-x-0"
leaveTo="translate-x-full"
>
<div className="w-screen max-w-2xl border-l dark:border-gray-700">
<Form
initialValues={{
id: client.id,
name: client.name,
type: client.type,
enabled: client.enabled,
host: client.host,
port: client.port,
ssl: client.ssl,
username: client.username,
password: client.password,
settings: client.settings,
}}
onSubmit={onSubmit}
>
{({ handleSubmit, values }) => {
return (
<form
className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll"
onSubmit={handleSubmit}
>
<div className="flex-1">
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
<div className="flex items-start justify-between space-x-3">
<div className="space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">
Edit client
</Dialog.Title>
<p className="text-sm text-gray-500 dark:text-gray-400">
Edit download client settings.
</p>
</div>
<div className="h-7 flex items-center">
<button
type="button"
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
onClick={toggle}
>
<span className="sr-only">Close panel</span>
<XIcon
className="h-6 w-6"
aria-hidden="true"
/>
</button>
</div>
</div>
</div>
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700">
<TextFieldWide name="name" label="Name" />
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="enabled" label="Enabled" />
</div>
<RadioFieldsetWide
name="type"
legend="Type"
options={DownloadClientTypeOptions}
/>
<div>{componentMap[values.type]}</div>
</div>
</div>
{rulesComponentMap[values.type]}
<div className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6">
<div className="space-x-3 flex justify-between">
<button
type="button"
className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
onClick={toggleDeleteModal}
>
Remove
</button>
<div className="flex">
<button
type="button"
className={classNames(
isSuccessfulTest
? "text-green-500 border-green-500 bg-green-50"
: isErrorTest
? "text-red-500 border-red-500 bg-red-50"
: "border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-400 bg-white dark:bg-gray-700 hover:bg-gray-50 focus:border-rose-700 active:bg-rose-700",
isTesting ? "cursor-not-allowed" : "",
"mr-2 inline-flex items-center px-4 py-2 border font-medium rounded-md shadow-sm text-sm transition ease-in-out duration-150 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
)}
disabled={isTesting}
onClick={() => testClient(values)}
>
{isTesting ? (
<svg
className="animate-spin h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
) : isSuccessfulTest ? (
"OK!"
) : isErrorTest ? (
"ERROR"
) : (
"Test"
)}
</button>
<button
type="button"
className="mr-4 bg-white dark:bg-gray-700 py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
onClick={toggle}
>
Cancel
</button>
<button
type="submit"
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
>
Create
</button>
</div>
</div>
</div>
<DEBUG values={values} />
</form>
);
}}
</Form>
</div>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
}

View file

@ -13,12 +13,14 @@ import APIClient from "../../api/APIClient";
import { NumberFieldWide, PasswordFieldWide } from "../../components/inputs/wide"; import { NumberFieldWide, PasswordFieldWide } from "../../components/inputs/wide";
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import Toast from '../../components/notifications/Toast'; import Toast from '../../components/notifications/Toast';
interface props { import { SlideOver } from "../../components/panels";
interface AddProps {
isOpen: boolean; isOpen: boolean;
toggle: any; toggle: any;
} }
function IndexerAddForm({ isOpen, toggle }: props) { export function IndexerAddForm({ isOpen, toggle }: AddProps) {
const { data } = useQuery<IndexerSchema[], Error>('indexerSchema', APIClient.indexers.getSchema, const { data } = useQuery<IndexerSchema[], Error>('indexerSchema', APIClient.indexers.getSchema,
{ {
enabled: isOpen, enabled: isOpen,
@ -40,7 +42,7 @@ function IndexerAddForm({ isOpen, toggle }: props) {
const ircMutation = useMutation((network: Network) => APIClient.irc.createNetwork(network), { const ircMutation = useMutation((network: Network) => APIClient.irc.createNetwork(network), {
onSuccess: (data) => { onSuccess: (data) => {
console.log("irc mutation: ", data); // console.log("irc mutation: ", data);
// queryClient.invalidateQueries(['indexer']); // queryClient.invalidateQueries(['indexer']);
// sleep(1500) // sleep(1500)
@ -97,13 +99,14 @@ function IndexerAddForm({ isOpen, toggle }: props) {
switch (f.type) { switch (f.type) {
case "text": case "text":
return ( return (
<TextFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} defaultValue=""/> <TextFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} defaultValue="" />
) )
case "secret": case "secret":
return ( return (
<PasswordFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} defaultValue="" /> <PasswordFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} defaultValue="" />
) )
} }
return null
})} })}
<div hidden={true}> <div hidden={true}>
<TextFieldWide name={`name`} label="Name" defaultValue={ind?.name} /> <TextFieldWide name={`name`} label="Name" defaultValue={ind?.name} />
@ -121,10 +124,10 @@ function IndexerAddForm({ isOpen, toggle }: props) {
return ( return (
<Fragment> <Fragment>
{ind && ind.irc && ind.irc.settings && ( {ind && ind.irc && ind.irc.settings && (
<div className="border-t border-gray-200 py-5"> <div className="border-t border-gray-200 dark:border-gray-700 py-5">
<div className="px-6 space-y-1"> <div className="px-6 space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900">IRC</Dialog.Title> <Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">IRC</Dialog.Title>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500 dark:text-gray-200">
Networks, channels and invite commands are configured automatically. Networks, channels and invite commands are configured automatically.
</p> </p>
</div> </div>
@ -135,6 +138,7 @@ function IndexerAddForm({ isOpen, toggle }: props) {
case "secret": case "secret":
return <PasswordFieldWide name={`irc.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} defaultValue={f.default} /> return <PasswordFieldWide name={`irc.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} defaultValue={f.default} />
} }
return null
})} })}
<div hidden={true}> <div hidden={true}>
@ -165,7 +169,7 @@ function IndexerAddForm({ isOpen, toggle }: props) {
leaveFrom="translate-x-0" leaveFrom="translate-x-0"
leaveTo="translate-x-full" leaveTo="translate-x-full"
> >
<div className="w-screen max-w-2xl"> <div className="w-screen max-w-2xl dark:border-gray-700 border-l">
<Form <Form
initialValues={{ initialValues={{
enabled: true, enabled: true,
@ -176,23 +180,23 @@ function IndexerAddForm({ isOpen, toggle }: props) {
> >
{({ handleSubmit, values }) => { {({ handleSubmit, values }) => {
return ( return (
<form className="h-full flex flex-col bg-white shadow-xl overflow-y-scroll" <form className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll"
onSubmit={handleSubmit}> onSubmit={handleSubmit}>
<div className="flex-1"> <div className="flex-1">
<div className="px-4 py-6 bg-gray-50 sm:px-6"> <div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
<div className="flex items-start justify-between space-x-3"> <div className="flex items-start justify-between space-x-3">
<div className="space-y-1"> <div className="space-y-1">
<Dialog.Title <Dialog.Title
className="text-lg font-medium text-gray-900">Add className="text-lg font-medium text-gray-900 dark:text-white">Add
indexer</Dialog.Title> indexer</Dialog.Title>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500 dark:text-gray-200">
Add indexer. Add indexer.
</p> </p>
</div> </div>
<div className="h-7 flex items-center"> <div className="h-7 flex items-center">
<button <button
type="button" type="button"
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500" className="bg-white dark:bg-gray-700 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
onClick={toggle} onClick={toggle}
> >
<span className="sr-only">Close panel</span> <span className="sr-only">Close panel</span>
@ -203,14 +207,14 @@ function IndexerAddForm({ isOpen, toggle }: props) {
</div> </div>
<div <div
className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200"> className="py-6 space-y-6 py-0 space-y-0 divide-y divide-gray-200 dark:divide-gray-700">
<div <div
className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5"> className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
<div> <div>
<label <label
htmlFor="identifier" htmlFor="identifier"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2" className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2"
> >
Indexer Indexer
</label> </label>
@ -249,18 +253,18 @@ function IndexerAddForm({ isOpen, toggle }: props) {
</div> </div>
<div <div
className="flex-shrink-0 px-4 border-t border-gray-200 py-5 sm:px-6"> className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6">
<div className="space-x-3 flex justify-end"> <div className="space-x-3 flex justify-end">
<button <button
type="button" type="button"
className="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" className="bg-white dark:bg-gray-700 py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
onClick={toggle} onClick={toggle}
> >
Cancel Cancel
</button> </button>
<button <button
type="submit" type="submit"
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
> >
Save Save
</button> </button>
@ -282,4 +286,120 @@ function IndexerAddForm({ isOpen, toggle }: props) {
) )
} }
export default IndexerAddForm; interface UpdateProps {
isOpen: boolean;
toggle: any;
indexer: Indexer;
}
export function IndexerUpdateForm({ isOpen, toggle, indexer }: UpdateProps) {
const mutation = useMutation((indexer: Indexer) => APIClient.indexers.update(indexer), {
onSuccess: () => {
queryClient.invalidateQueries(['indexer']);
toast.custom((t) => <Toast type="success" body={`${indexer.name} was updated successfully`} t={t} />)
sleep(1500)
toggle()
}
})
const deleteMutation = useMutation((id: number) => APIClient.indexers.delete(id), {
onSuccess: () => {
queryClient.invalidateQueries(['indexer']);
toast.custom((t) => <Toast type="success" body={`${indexer.name} was deleted.`} t={t} />)
}
})
const onSubmit = (data: any) => {
// TODO clear data depending on type
mutation.mutate(data)
};
const deleteAction = () => {
deleteMutation.mutate(indexer.id)
}
const renderSettingFields = (settings: any[]) => {
if (settings !== []) {
return (
<div key="opt">
{settings && settings.map((f: any, idx: number) => {
switch (f.type) {
case "text":
return (
<TextFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} />
)
case "secret":
return (
<PasswordFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} />
)
}
return null
})}
</div>
)
}
}
let initialValues = {
id: indexer.id,
name: indexer.name,
enabled: indexer.enabled,
identifier: indexer.identifier,
settings: indexer.settings.reduce((o: any, obj: any) => ({ ...o, [obj.name]: obj.value }), {}),
}
return (
<SlideOver
type="UPDATE"
title="Indexer"
isOpen={isOpen}
toggle={toggle}
deleteAction={deleteAction}
onSubmit={onSubmit}
initialValues={initialValues}
>
{({ values }: any) => (
<>
<div
className="py-6 space-y-6 sm:py-0 sm:space-y-0 divide-y divide-gray-200 dark:divide-gray-700">
<div
className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
<div>
<label
htmlFor="name"
className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2"
>
Name
</label>
</div>
<Field name="name">
{({ input, meta }) => (
<div className="sm:col-span-2">
<input
type="text"
{...input}
className="block w-full shadow-sm dark:bg-gray-800 sm:text-sm dark:text-white focus:ring-indigo-500 focus:border-indigo-500 border-gray-300 dark:border-gray-700 rounded-md"
/>
{meta.touched && meta.error &&
<span>{meta.error}</span>}
</div>
)}
</Field>
</div>
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200 dark:sm:divide-gray-700">
<SwitchGroup name="enabled" label="Enabled" />
</div>
{renderSettingFields(indexer.settings)}
</div>
</>
)}
</SlideOver>
)
}

View file

@ -1,288 +0,0 @@
import { Fragment, useRef } from "react";
import { useMutation } from "react-query";
import { Indexer } from "../../domain/interfaces";
import { sleep } from "../../utils/utils";
import { ExclamationIcon, XIcon } from "@heroicons/react/solid";
import { Dialog, Transition } from "@headlessui/react";
import { Field, Form } from "react-final-form";
import DEBUG from "../../components/debug";
import { SwitchGroup, TextFieldWide } from "../../components/inputs";
import { useToggle } from "../../hooks/hooks";
import APIClient from "../../api/APIClient";
import { queryClient } from "../../App";
import { PasswordFieldWide } from "../../components/inputs/wide";
import { toast } from 'react-hot-toast'
import Toast from '../../components/notifications/Toast';
interface props {
isOpen: boolean;
toggle: any;
indexer: Indexer;
}
function IndexerUpdateForm({ isOpen, toggle, indexer }: props) {
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false)
const mutation = useMutation((indexer: Indexer) => APIClient.indexers.update(indexer), {
onSuccess: () => {
queryClient.invalidateQueries(['indexer']);
toast.custom((t) => <Toast type="success" body={`${indexer.name} was updated successfully`} t={t}/>)
sleep(1500)
toggle()
}
})
const deleteMutation = useMutation((id: number) => APIClient.indexers.delete(id), {
onSuccess: () => {
queryClient.invalidateQueries(['indexer']);
toast.custom((t) => <Toast type="success" body={`${indexer.name} was deleted.`} t={t}/>)
}
})
const cancelModalButtonRef = useRef(null)
const onSubmit = (data: any) => {
// TODO clear data depending on type
mutation.mutate(data)
};
const deleteAction = () => {
deleteMutation.mutate(indexer.id)
}
const renderSettingFields = (settings: any[]) => {
if (settings !== []) {
return (
<div key="opt">
{settings && settings.map((f: any, idx: number) => {
switch (f.type) {
case "text":
return (
<TextFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} />
)
case "secret":
return (
<PasswordFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} />
)
}
})}
</div>
)
}
}
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" static className="fixed inset-0 overflow-hidden" open={isOpen} onClose={toggle}>
<Transition.Root show={deleteModalIsOpen} as={Fragment}>
<Dialog
as="div"
static
className="fixed z-10 inset-0 overflow-y-auto"
initialFocus={cancelModalButtonRef}
open={deleteModalIsOpen}
onClose={toggleDeleteModal}
>
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
{/* This element is to trick the browser into centering the modal contents. */}
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203;
</span>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<ExclamationIcon className="h-6 w-6 text-red-600" aria-hidden="true" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<Dialog.Title as="h3" className="text-lg leading-6 font-medium text-gray-900">
Remove indexer
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">
Are you sure you want to remove this indexer?
This action cannot be undone.
</p>
</div>
</div>
</div>
</div>
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button
type="button"
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm"
onClick={deleteAction}
>
Remove
</button>
<button
type="button"
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
onClick={toggleDeleteModal}
ref={cancelModalButtonRef}
>
Cancel
</button>
</div>
</div>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
<div className="absolute inset-0 overflow-hidden">
<Dialog.Overlay className="absolute inset-0" />
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
<Transition.Child
as={Fragment}
enter="transform transition ease-in-out duration-500 sm:duration-700"
enterFrom="translate-x-full"
enterTo="translate-x-0"
leave="transform transition ease-in-out duration-500 sm:duration-700"
leaveFrom="translate-x-0"
leaveTo="translate-x-full"
>
<div className="w-screen max-w-2xl">
<Form
initialValues={{
id: indexer.id,
name: indexer.name,
enabled: indexer.enabled,
identifier: indexer.identifier,
settings: indexer.settings.reduce((o: any, obj: any) => ({ ...o, [obj.name]: obj.value }), {}),
}}
onSubmit={onSubmit}
>
{({ handleSubmit, values }) => {
return (
<form className="h-full flex flex-col bg-white shadow-xl overflow-y-scroll"
onSubmit={handleSubmit}>
<div className="flex-1">
<div className="px-4 py-6 bg-gray-50 sm:px-6">
<div className="flex items-start justify-between space-x-3">
<div className="space-y-1">
<Dialog.Title
className="text-lg font-medium text-gray-900">Update
indexer</Dialog.Title>
<p className="text-sm text-gray-500">
Update indexer.
</p>
</div>
<div className="h-7 flex items-center">
<button
type="button"
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
onClick={toggle}
>
<span className="sr-only">Close panel</span>
<XIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
</div>
</div>
<div
className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<div
className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
<div>
<label
htmlFor="name"
className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2"
>
Name
</label>
</div>
<Field name="name">
{({ input, meta }) => (
<div className="sm:col-span-2">
<input
type="text"
{...input}
className="block w-full shadow-sm sm:text-sm focus:ring-indigo-500 focus:border-indigo-500 border-gray-300 rounded-md"
/>
{meta.touched && meta.error &&
<span>{meta.error}</span>}
</div>
)}
</Field>
</div>
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="enabled" label="Enabled" />
</div>
{renderSettingFields(indexer.settings)}
</div>
</div>
<div className="flex-shrink-0 px-4 border-t border-gray-200 py-5 sm:px-6">
<div className="space-x-3 flex justify-between">
<button
type="button"
className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
onClick={toggleDeleteModal}
>
Remove
</button>
<div>
<button
type="button"
className="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
onClick={toggle}
>
Cancel
</button>
<button
type="submit"
className="ml-4 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Save
</button>
</div>
</div>
</div>
<DEBUG values={values} />
</form>
)
}}
</Form>
</div>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
)
}
export default IndexerUpdateForm;

View file

@ -0,0 +1,347 @@
import { useMutation } from "react-query";
import { Network } from "../../domain/interfaces";
import { XIcon } from "@heroicons/react/solid";
import { Field } from "react-final-form";
import { SwitchGroup, TextFieldWide } from "../../components/inputs";
import { queryClient } from "../../App";
import arrayMutators from "final-form-arrays";
import { FieldArray } from "react-final-form-arrays";
import APIClient from "../../api/APIClient";
import { NumberFieldWide, PasswordFieldWide } from "../../components/inputs/wide";
import { toast } from 'react-hot-toast';
import Toast from '../../components/notifications/Toast';
import { SlideOver } from "../../components/panels";
export function IrcNetworkAddForm({ isOpen, toggle }: any) {
const mutation = useMutation((network: Network) => APIClient.irc.createNetwork(network), {
onSuccess: (data) => {
queryClient.invalidateQueries(['networks']);
toast.custom((t) => <Toast type="success" body="IRC Network added" t={t} />)
toggle()
},
onError: () => {
toast.custom((t) => <Toast type="error" body="IRC Network could not be added" t={t} />)
},
})
const onSubmit = (data: any) => {
// easy way to split textarea lines into array of strings for each newline.
// parse on the field didn't really work.
let cmds = data.connect_commands && data.connect_commands.length > 0 ? data.connect_commands.replace(/\r\n/g, "\n").split("\n") : [];
data.connect_commands = cmds
console.log("formated", data)
mutation.mutate(data)
};
const validate = (values: any) => {
const errors = {
nickserv: {
account: null,
}
} as any;
if (!values.name) {
errors.name = "Required";
}
if (!values.port) {
errors.port = "Required";
}
if (!values.server) {
errors.server = "Required";
}
if (!values.nickserv?.account) {
errors.nickserv.account = "Required";
}
return errors;
}
const initialValues = {
name: "",
enabled: true,
server: "",
tls: false,
pass: "",
nickserv: {
account: ""
}
}
const mutators = {
...arrayMutators
}
return (
<SlideOver
type="CREATE"
title="Network"
isOpen={isOpen}
toggle={toggle}
onSubmit={onSubmit}
initialValues={initialValues}
mutators={mutators}
validate={validate}
>
{() => (
<>
<TextFieldWide name="name" label="Name" placeholder="Name" required={true} />
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700">
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200 dark:sm:divide-gray-700">
<SwitchGroup name="enabled" label="Enabled" />
</div>
<div>
<TextFieldWide name="server" label="Server" placeholder="Address: Eg irc.server.net" required={true} />
<NumberFieldWide name="port" label="Port" placeholder="Eg 6667" required={true} />
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="tls" label="TLS" />
</div>
<PasswordFieldWide name="pass" label="Password" help="Network password" />
<TextFieldWide name="nickserv.account" label="NickServ Account" placeholder="NickServ Account" required={true} />
<PasswordFieldWide name="nickserv.password" label="NickServ Password" />
<PasswordFieldWide name="invite_command" label="Invite command" />
</div>
</div>
<div className="p-6">
<FieldArray name="channels">
{({ fields }) => (
<div className="flex flex-col border-2 border-dashed dark:border-gray-700 p-4">
{fields && (fields.length as any) > 0 ? (
fields.map((name, index) => (
<div key={name} className="flex justify-between">
<div className="flex">
<Field
name={`${name}.name`}
component="input"
type="text"
placeholder="#Channel"
className="mr-4 dark:bg-gray-700 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-600 block w-full shadow-sm sm:text-sm dark:text-white rounded-md"
/>
<Field
name={`${name}.password`}
component="input"
type="text"
placeholder="Password"
className="dark:bg-gray-700 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-600 block w-full shadow-sm sm:text-sm dark:text-white rounded-md"
/>
</div>
<button
type="button"
className="bg-white dark:bg-gray-700 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
onClick={() => fields.remove(index)}
>
<span className="sr-only">Remove</span>
<XIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
))
) : (
<span className="text-center text-sm text-grey-darker dark:text-white">
No channels!
</span>
)}
<button
type="button"
className="border dark:border-gray-600 dark:bg-gray-700 my-4 px-4 py-2 text-sm text-gray-700 dark:text-white hover:bg-gray-50 dark:hover:bg-gray-600 rounded self-center text-center"
onClick={() => fields.push({ name: "", password: "" })}
>
Add Channel
</button>
</div>
)}
</FieldArray>
</div>
</>
)}
</SlideOver>
)
}
export function IrcNetworkUpdateForm({ isOpen, toggle, network }: any) {
const mutation = useMutation((network: Network) => APIClient.irc.updateNetwork(network), {
onSuccess: () => {
queryClient.invalidateQueries(['networks']);
toast.custom((t) => <Toast type="success" body={`${network.name} was updated successfully`} t={t} />)
toggle()
}
})
const deleteMutation = useMutation((id: number) => APIClient.irc.deleteNetwork(id), {
onSuccess: () => {
queryClient.invalidateQueries(['networks']);
toast.custom((t) => <Toast type="success" body={`${network.name} was deleted.`} t={t} />)
toggle()
}
})
const onSubmit = (data: any) => {
console.log(data)
// easy way to split textarea lines into array of strings for each newline.
// parse on the field didn't really work.
// TODO fix connect_commands on network update
// let cmds = data.connect_commands && data.connect_commands.length > 0 ? data.connect_commands.replace(/\r\n/g,"\n").split("\n") : [];
// data.connect_commands = cmds
// console.log("formatted", data)
mutation.mutate(data)
};
const validate = (values: any) => {
const errors = {} as any;
if (!values.name) {
errors.name = "Required";
}
if (!values.server) {
errors.server = "Required";
}
if (!values.port) {
errors.port = "Required";
}
if (!values.nickserv?.account) {
errors.nickserv.account = "Required";
}
return errors;
}
const deleteAction = () => {
deleteMutation.mutate(network.id)
}
const initialValues = {
id: network.id,
name: network.name,
enabled: network.enabled,
server: network.server,
port: network.port,
tls: network.tls,
nickserv: network.nickserv,
pass: network.pass,
invite_command: network.invite_command,
// connect_commands: network.connect_commands,
channels: network.channels
}
const mutators = {
...arrayMutators
}
return (
<SlideOver
type="UPDATE"
title="Network"
isOpen={isOpen}
toggle={toggle}
onSubmit={onSubmit}
deleteAction={deleteAction}
initialValues={initialValues}
mutators={mutators}
validate={validate}
>
{() => (
<>
<TextFieldWide name="name" label="Name" placeholder="Name" required={true} />
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700">
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0">
<SwitchGroup name="enabled" label="Enabled" />
</div>
<div>
<TextFieldWide name="server" label="Server" placeholder="Address: Eg irc.server.net" required={true} />
<NumberFieldWide name="port" label="Port" placeholder="Eg 6667" required={true} />
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="tls" label="TLS" />
</div>
<PasswordFieldWide name="pass" label="Password" help="Network password" />
<TextFieldWide name="nickserv.account" label="NickServ Account" placeholder="NickServ Account" required={true} />
<PasswordFieldWide name="nickserv.password" label="NickServ Password" />
<PasswordFieldWide name="invite_command" label="Invite command" />
</div>
</div>
<div className="p-6">
<FieldArray name="channels">
{({ fields }) => (
<div className="flex flex-col border-2 border-dashed dark:border-gray-700 p-4">
{fields && (fields.length as any) > 0 ? (
fields.map((name, index) => (
<div key={name} className="flex justify-between">
<div className="flex">
<Field
name={`${name}.name`}
component="input"
type="text"
placeholder="#Channel"
className="mr-4 dark:bg-gray-700 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-600 block w-full shadow-sm sm:text-sm dark:text-white rounded-md"
/>
<Field
name={`${name}.password`}
component="input"
type="text"
placeholder="Password"
className="dark:bg-gray-700 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-600 block w-full shadow-sm sm:text-sm dark:text-white rounded-md"
/>
</div>
<button
type="button"
className="bg-white dark:bg-gray-700 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
onClick={() => fields.remove(index)}
>
<span className="sr-only">Remove</span>
<XIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
))
) : (
<span className="text-center text-sm text-grey-darker dark:text-white">
No channels!
</span>
)}
<button
type="button"
className="border dark:border-gray-600 dark:bg-gray-700 my-4 px-4 py-2 text-sm text-gray-700 dark:text-white hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-blue-500 rounded self-center text-center"
onClick={() => fields.push({ name: "", password: "" })}
>
Add Channel
</button>
</div>
)}
</FieldArray>
</div>
</>
)}
</SlideOver>
)
}

View file

@ -1,254 +0,0 @@
import {Fragment} from "react";
import {useMutation} from "react-query";
import {Network} from "../../domain/interfaces";
import {Dialog, Transition} from "@headlessui/react";
import {XIcon} from "@heroicons/react/solid";
import {Field, Form} from "react-final-form";
import DEBUG from "../../components/debug";
import {SwitchGroup, TextFieldWide} from "../../components/inputs";
import {queryClient} from "../../App";
import arrayMutators from "final-form-arrays";
import { FieldArray } from "react-final-form-arrays";
import {classNames} from "../../styles/utils";
import APIClient from "../../api/APIClient";
import { NumberFieldWide, PasswordFieldWide } from "../../components/inputs/wide";
import { toast } from 'react-hot-toast';
import Toast from '../../components/notifications/Toast';
type FormValues = {
name: string
server: string
nickserv: {
account: string
}
port: number
}
function IrcNetworkAddForm({isOpen, toggle}: any) {
const mutation = useMutation((network: Network) => APIClient.irc.createNetwork(network), {
onSuccess: (data) => {
queryClient.invalidateQueries(['networks']);
toast.custom((t) => <Toast type="success" body="IRC Network added" t={t} />)
toggle()
},
onError: () => {
toast.custom((t) => <Toast type="error" body="IRC Network could not be added" t={t}/>)
},
})
const onSubmit = (data: any) => {
// easy way to split textarea lines into array of strings for each newline.
// parse on the field didn't really work.
let cmds = data.connect_commands && data.connect_commands.length > 0 ? data.connect_commands.replace(/\r\n/g,"\n").split("\n") : [];
data.connect_commands = cmds
console.log("formated", data)
mutation.mutate(data)
};
const validate = (values: any) => {
const errors = {
nickserv: {
account: null,
}
} as any;
if (!values.name) {
errors.name = "Required";
}
if (!values.port) {
errors.port = "Required";
}
if (!values.server) {
errors.server = "Required";
}
if(!values.nickserv?.account) {
errors.nickserv.account = "Required";
}
return errors;
}
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" static className="fixed inset-0 overflow-hidden transition-all" open={isOpen} onClose={toggle}>
<div className="absolute inset-0 overflow-hidden">
<Dialog.Overlay className="absolute inset-0"/>
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
<Transition.Child
as={Fragment}
enter="transform transition ease-in-out duration-500 sm:duration-700"
enterFrom="translate-x-full"
enterTo="translate-x-0"
leave="transform transition ease-in-out duration-500 sm:duration-700"
leaveFrom="translate-x-0"
leaveTo="translate-x-full"
>
<div className="w-screen max-w-2xl">
<Form
initialValues={{
name: "",
enabled: true,
server: "",
tls: false,
pass: "",
nickserv: {
account: ""
}
}}
mutators={{
...arrayMutators
}}
validate={validate}
onSubmit={onSubmit}
>
{({handleSubmit, values, pristine, invalid}) => {
return (
<form className="h-full flex flex-col bg-white shadow-xl overflow-y-scroll"
onSubmit={handleSubmit}>
<div className="flex-1">
<div className="px-4 py-6 bg-gray-50 sm:px-6">
<div className="flex items-start justify-between space-x-3">
<div className="space-y-1">
<Dialog.Title
className="text-lg font-medium text-gray-900">Add
network</Dialog.Title>
<p className="text-sm text-gray-500">
Add irc network.
</p>
</div>
<div className="h-7 flex items-center">
<button
type="button"
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
onClick={toggle}
>
<span className="sr-only">Close panel</span>
<XIcon className="h-6 w-6" aria-hidden="true"/>
</button>
</div>
</div>
</div>
<TextFieldWide name="name" label="Name" placeholder="Name" required={true} />
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<div
className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="enabled" label="Enabled"/>
</div>
<div>
<TextFieldWide name="server" label="Server" placeholder="Address: Eg irc.server.net" required={true} />
<NumberFieldWide name="port" label="Port" placeholder="Eg 6667" required={true} />
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="tls" label="TLS"/>
</div>
<PasswordFieldWide name="pass" label="Password" help="Network password" />
<TextFieldWide name="nickserv.account" label="NickServ Account" placeholder="NickServ Account" required={true} />
<PasswordFieldWide name="nickserv.password" label="NickServ Password" />
<PasswordFieldWide name="invite_command" label="Invite command" />
</div>
</div>
<div className="p-6">
<FieldArray name="channels">
{({ fields }) => (
<div className="flex flex-col border-2 border-dashed p-4">
{fields && (fields.length as any) > 0 ? (
fields.map((name, index) => (
<div key={name} className="flex justify-between">
<div className="flex">
<Field
name={`${name}.name`}
component="input"
type="text"
placeholder="#Channel"
className="mr-4 focus:ring-indigo-500 focus:border-indigo-500 border-gray-300 block w-full shadow-sm sm:text-sm rounded-md"
/>
<Field
name={`${name}.password`}
component="input"
type="text"
placeholder="Password"
className="focus:ring-indigo-500 focus:border-indigo-500 border-gray-300 block w-full shadow-sm sm:text-sm rounded-md"
/>
</div>
<button
type="button"
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
onClick={() => fields.remove(index)}
>
<span className="sr-only">Remove</span>
<XIcon className="h-6 w-6" aria-hidden="true"/>
</button>
</div>
))
) : (
<span className="text-center text-sm text-grey-darker">
No channels!
</span>
)}
<button
type="button"
className="border my-4 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded self-center text-center"
onClick={() => fields.push({ name: "", password: "" })}
>
Add Channel
</button>
</div>
)}
</FieldArray>
</div>
</div>
<div
className="flex-shrink-0 px-4 border-t border-gray-200 py-5 sm:px-6">
<div className="space-x-3 flex justify-end">
<button
type="button"
className="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
onClick={toggle}
>
Cancel
</button>
<button
type="submit"
disabled={pristine || invalid}
className={classNames(pristine || invalid ? "bg-indigo-300" : "bg-indigo-600 hover:bg-indigo-700","inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500")}
>
Create
</button>
</div>
</div>
<DEBUG values={values}/>
</form>
)
}}
</Form>
</div>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
)
}
export default IrcNetworkAddForm;

View file

@ -1,297 +0,0 @@
import { Fragment, useEffect, useRef } from "react";
import { useMutation } from "react-query";
import { Network } from "../../domain/interfaces";
import { Dialog, Transition } from "@headlessui/react";
import { XIcon } from "@heroicons/react/solid";
import { Field, Form } from "react-final-form";
import DEBUG from "../../components/debug";
import { SwitchGroup, TextFieldWide } from "../../components/inputs";
import { queryClient } from "../../App";
import arrayMutators from "final-form-arrays";
import { FieldArray } from "react-final-form-arrays";
import { classNames } from "../../styles/utils";
import { useToggle } from "../../hooks/hooks";
import { DeleteModal } from "../../components/modals";
import APIClient from "../../api/APIClient";
import { NumberFieldWide, PasswordFieldWide } from "../../components/inputs/wide";
import { toast } from 'react-hot-toast';
import Toast from '../../components/notifications/Toast';
function IrcNetworkUpdateForm({ isOpen, toggle, network }: any) {
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false)
const mutation = useMutation((network: Network) => APIClient.irc.updateNetwork(network), {
onSuccess: () => {
queryClient.invalidateQueries(['networks']);
toast.custom((t) => <Toast type="success" body={`${network.name} was updated successfully`} t={t} />)
toggle()
}
})
const deleteMutation = useMutation((id: number) => APIClient.irc.deleteNetwork(id), {
onSuccess: () => {
queryClient.invalidateQueries(['networks']);
toast.custom((t) => <Toast type="success" body={`${network.name} was deleted.`} t={t} />)
toggle()
}
})
useEffect(() => {
console.log("render add network form")
}, []);
const onSubmit = (data: any) => {
console.log(data)
// easy way to split textarea lines into array of strings for each newline.
// parse on the field didn't really work.
// TODO fix connect_commands on network update
// let cmds = data.connect_commands && data.connect_commands.length > 0 ? data.connect_commands.replace(/\r\n/g,"\n").split("\n") : [];
// data.connect_commands = cmds
// console.log("formatted", data)
mutation.mutate(data)
};
const validate = (values: any) => {
const errors = {} as any;
if (!values.name) {
errors.name = "Required";
}
if (!values.server) {
errors.server = "Required";
}
if (!values.port) {
errors.port = "Required";
}
if(!values.nickserv?.account) {
errors.nickserv.account = "Required";
}
return errors;
}
const cancelModalButtonRef = useRef(null)
const deleteAction = () => {
deleteMutation.mutate(network.id)
}
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" static className="fixed inset-0 overflow-hidden" open={isOpen} onClose={toggle}>
<DeleteModal
isOpen={deleteModalIsOpen}
toggle={toggleDeleteModal}
buttonRef={cancelModalButtonRef}
deleteAction={deleteAction}
title="Remove network"
text="Are you sure you want to remove this network and channels? This action cannot be undone."
/>
<div className="absolute inset-0 overflow-hidden">
<Dialog.Overlay className="absolute inset-0" />
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
<Transition.Child
as={Fragment}
enter="transform transition ease-in-out duration-500 sm:duration-700"
enterFrom="translate-x-full"
enterTo="translate-x-0"
leave="transform transition ease-in-out duration-500 sm:duration-700"
leaveFrom="translate-x-0"
leaveTo="translate-x-full"
>
<div className="w-screen max-w-2xl">
<Form
initialValues={{
id: network.id,
name: network.name,
enabled: network.enabled,
server: network.server,
port: network.port,
tls: network.tls,
nickserv: network.nickserv,
pass: network.pass,
invite_command: network.invite_command,
// connect_commands: network.connect_commands,
channels: network.channels
}}
mutators={{
...arrayMutators
}}
validate={validate}
onSubmit={onSubmit}
>
{({ handleSubmit, values, pristine, invalid }) => {
return (
<form className="h-full flex flex-col bg-white shadow-xl overflow-y-scroll"
onSubmit={handleSubmit}>
<div className="flex-1">
{/* Header */}
<div className="px-4 py-6 bg-gray-50 sm:px-6">
<div className="flex items-start justify-between space-x-3">
<div className="space-y-1">
<Dialog.Title
className="text-lg font-medium text-gray-900">Update network</Dialog.Title>
<p className="text-sm text-gray-500">
Update irc network.
</p>
</div>
<div className="h-7 flex items-center">
<button
type="button"
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
onClick={toggle}
>
<span className="sr-only">Close panel</span>
<XIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
</div>
</div>
<TextFieldWide name="name" label="Name" placeholder="Name" required={true} />
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<div
className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="enabled" label="Enabled" />
</div>
<div>
<div className="px-6 space-y-1 mt-6">
<Dialog.Title className="text-lg font-medium text-gray-900">Connection</Dialog.Title>
{/* <p className="text-sm text-gray-500">
Networks, channels and invite commands are configured automatically.
</p> */}
</div>
<TextFieldWide name="server" label="Server" placeholder="Address: Eg irc.server.net" required={true} />
<NumberFieldWide name="port" label="Port" />
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="tls" label="TLS" />
</div>
<PasswordFieldWide name="pass" label="Password" help="Network password" />
<div className="px-6 space-y-1 border-t pt-6">
<Dialog.Title className="text-lg font-medium text-gray-900">Account</Dialog.Title>
{/* <p className="text-sm text-gray-500">
Networks, channels and invite commands are configured automatically.
</p> */}
</div>
<TextFieldWide name="nickserv.account" label="NickServ Account" required={true} />
<PasswordFieldWide name="nickserv.password" label="NickServ Password" />
<PasswordFieldWide name="invite_command" label="Invite command" />
</div>
</div>
<div className="p-6">
<FieldArray name="channels">
{({ fields }) => (
<div className="flex flex-col border-2 border-dashed p-4">
{fields && (fields.length as any) > 0 ? (
fields.map((name, index) => (
<div key={name} className="flex justify-between">
<div className="flex">
<Field
name={`${name}.name`}
component="input"
type="text"
placeholder="#Channel"
className="focus:ring-indigo-500 focus:border-indigo-500 border-gray-300 block w-full shadow-sm sm:text-sm rounded-md"
/>
<Field
name={`${name}.password`}
component="input"
type="text"
placeholder="Password"
className="focus:ring-indigo-500 focus:border-indigo-500 border-gray-300 block w-full shadow-sm sm:text-sm rounded-md"
/>
</div>
<button
type="button"
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
onClick={() => fields.remove(index)}
>
<span className="sr-only">Remove</span>
<XIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
))
) : (
<span className="text-center text-sm text-grey-darker">
No channels!
</span>
)}
<button
type="button"
className="border my-4 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded self-center text-center"
onClick={() => fields.push({ name: "", password: "" })}
>
Add Channel
</button>
</div>
)}
</FieldArray>
</div>
</div>
<div className="flex-shrink-0 px-4 border-t border-gray-200 py-5 sm:px-6">
<div className="space-x-3 flex justify-between">
<button
type="button"
className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
onClick={toggleDeleteModal}
>
Remove
</button>
<div>
<button
type="button"
className="mr-4 bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
onClick={toggle}
>
Cancel
</button>
<button
type="submit"
// disabled={pristine || invalid}
className={classNames(pristine || invalid ? "bg-indigo-300" : "bg-indigo-600 hover:bg-indigo-700", "inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500")}
>
Save
</button>
</div>
</div>
</div>
<DEBUG values={values} />
</form>
)
}}
</Form>
</div>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
)
}
export default IrcNetworkUpdateForm;

View file

@ -1,245 +0,0 @@
import { Fragment, useState } from "react";
import { useMutation } from "react-query";
import {
DOWNLOAD_CLIENT_TYPES,
DownloadClient,
} from "../../../domain/interfaces";
import { Dialog, Transition } from "@headlessui/react";
import { XIcon } from "@heroicons/react/solid";
import { classNames } from "../../../styles/utils";
import { Form } from "react-final-form";
import DEBUG from "../../../components/debug";
import { SwitchGroup, TextFieldWide } from "../../../components/inputs";
import { queryClient } from "../../../App";
import APIClient from "../../../api/APIClient";
import { sleep } from "../../../utils/utils";
import { DownloadClientTypeOptions } from "../../../domain/constants";
import { RadioFieldsetWide } from "../../../components/inputs/wide";
import { componentMap, rulesComponentMap } from "./shared";
import { toast } from 'react-hot-toast'
import Toast from '../../../components/notifications/Toast';
function DownloadClientAddForm({ isOpen, toggle }: any) {
const [isTesting, setIsTesting] = useState(false);
const [isSuccessfulTest, setIsSuccessfulTest] = useState(false);
const [isErrorTest, setIsErrorTest] = useState(false);
const mutation = useMutation(
(client: DownloadClient) => APIClient.download_clients.create(client),
{
onSuccess: () => {
queryClient.invalidateQueries(["downloadClients"]);
toast.custom((t) => <Toast type="success" body="Client was added" t={t} />)
toggle();
},
onError: () => {
toast.custom((t) => <Toast type="error" body="Client could not be added" t={t} />)
}
}
);
const testClientMutation = useMutation(
(client: DownloadClient) => APIClient.download_clients.test(client),
{
onMutate: () => {
setIsTesting(true);
setIsErrorTest(false);
setIsSuccessfulTest(false);
},
onSuccess: () => {
sleep(1000)
.then(() => {
setIsTesting(false);
setIsSuccessfulTest(true);
})
.then(() => {
sleep(2500).then(() => {
setIsSuccessfulTest(false);
});
});
},
onError: (error) => {
console.log('not added')
setIsTesting(false);
setIsErrorTest(true);
sleep(2500).then(() => {
setIsErrorTest(false);
});
},
}
);
const onSubmit = (data: any) => {
mutation.mutate(data);
};
const testClient = (data: any) => {
testClientMutation.mutate(data);
};
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog
as="div"
static
className="fixed inset-0 overflow-hidden"
open={isOpen}
onClose={toggle}
>
<div className="absolute inset-0 overflow-hidden">
<Dialog.Overlay className="absolute inset-0" />
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
<Transition.Child
as={Fragment}
enter="transform transition ease-in-out duration-500 sm:duration-700"
enterFrom="translate-x-full"
enterTo="translate-x-0"
leave="transform transition ease-in-out duration-500 sm:duration-700"
leaveFrom="translate-x-0"
leaveTo="translate-x-full"
>
<div className="w-screen max-w-2xl">
<Form
initialValues={{
name: "",
type: DOWNLOAD_CLIENT_TYPES.qBittorrent,
enabled: true,
host: "",
port: 10000,
ssl: false,
username: "",
password: "",
}}
onSubmit={onSubmit}
>
{({ handleSubmit, values }) => {
return (
<form
className="h-full flex flex-col bg-white shadow-xl overflow-y-scroll"
onSubmit={handleSubmit}
>
<div className="flex-1">
<div className="px-4 py-6 bg-gray-50 sm:px-6">
<div className="flex items-start justify-between space-x-3">
<div className="space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900">
Add client
</Dialog.Title>
<p className="text-sm text-gray-500">
Add download client.
</p>
</div>
<div className="h-7 flex items-center">
<button
type="button"
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
onClick={toggle}
>
<span className="sr-only">Close panel</span>
<XIcon
className="h-6 w-6"
aria-hidden="true"
/>
</button>
</div>
</div>
</div>
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<TextFieldWide name="name" label="Name" />
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="enabled" label="Enabled" />
</div>
<RadioFieldsetWide
name="type"
legend="Type"
options={DownloadClientTypeOptions}
/>
<div>{componentMap[values.type]}</div>
</div>
</div>
{rulesComponentMap[values.type]}
<div className="flex-shrink-0 px-4 border-t border-gray-200 py-5 sm:px-6">
<div className="space-x-3 flex justify-end">
<button
type="button"
className={classNames(
isSuccessfulTest
? "text-green-500 border-green-500 bg-green-50"
: isErrorTest
? "text-red-500 border-red-500 bg-red-50"
: "border-gray-300 text-gray-700 bg-white hover:bg-gray-50 focus:border-rose-700 active:bg-rose-700",
isTesting ? "cursor-not-allowed" : "",
"mr-2 inline-flex items-center px-4 py-2 border font-medium rounded-md shadow-sm text-sm transition ease-in-out duration-150"
)}
disabled={isTesting}
onClick={() => testClient(values)}
>
{isTesting ? (
<svg
className="animate-spin h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
) : isSuccessfulTest ? (
"OK!"
) : isErrorTest ? (
"ERROR"
) : (
"Test"
)}
</button>
<button
type="button"
className="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
onClick={toggle}
>
Cancel
</button>
<button
type="submit"
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Create
</button>
</div>
</div>
<DEBUG values={values} />
</form>
);
}}
</Form>
</div>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
}
export default DownloadClientAddForm;

View file

@ -1,279 +0,0 @@
import { Fragment, useRef, useState } from "react";
import { useToggle } from "../../../hooks/hooks";
import { useMutation } from "react-query";
import { DownloadClient } from "../../../domain/interfaces";
import { queryClient } from "../../../App";
import { Dialog, Transition } from "@headlessui/react";
import { XIcon } from "@heroicons/react/solid";
import { classNames } from "../../../styles/utils";
import { Form } from "react-final-form";
import DEBUG from "../../../components/debug";
import { SwitchGroup, TextFieldWide } from "../../../components/inputs";
import { DownloadClientTypeOptions } from "../../../domain/constants";
import APIClient from "../../../api/APIClient";
import { sleep } from "../../../utils/utils";
import { componentMap, rulesComponentMap } from "./shared";
import { RadioFieldsetWide } from "../../../components/inputs/wide";
import { DeleteModal } from "../../../components/modals";
import { toast } from 'react-hot-toast'
import Toast from '../../../components/notifications/Toast';
function DownloadClientUpdateForm({ client, isOpen, toggle }: any) {
const [isTesting, setIsTesting] = useState(false);
const [isSuccessfulTest, setIsSuccessfulTest] = useState(false);
const [isErrorTest, setIsErrorTest] = useState(false);
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
const mutation = useMutation(
(client: DownloadClient) => APIClient.download_clients.update(client),
{
onSuccess: () => {
queryClient.invalidateQueries(["downloadClients"]);
toast.custom((t) => <Toast type="success" body={`${client.name} was updated successfully`} t={t} />)
toggle();
},
}
);
const deleteMutation = useMutation(
(clientID: number) => APIClient.download_clients.delete(clientID),
{
onSuccess: () => {
queryClient.invalidateQueries();
toast.custom((t) => <Toast type="success" body={`${client.name} was deleted.`} t={t}/>)
toggleDeleteModal();
},
}
);
const testClientMutation = useMutation(
(client: DownloadClient) => APIClient.download_clients.test(client),
{
onMutate: () => {
setIsTesting(true);
setIsErrorTest(false);
setIsSuccessfulTest(false);
},
onSuccess: () => {
sleep(1000)
.then(() => {
setIsTesting(false);
setIsSuccessfulTest(true);
})
.then(() => {
sleep(2500).then(() => {
setIsSuccessfulTest(false);
});
});
},
onError: (error) => {
setIsTesting(false);
setIsErrorTest(true);
sleep(2500).then(() => {
setIsErrorTest(false);
});
},
}
);
const onSubmit = (data: any) => {
mutation.mutate(data);
};
const cancelButtonRef = useRef(null);
const cancelModalButtonRef = useRef(null);
const deleteAction = () => {
deleteMutation.mutate(client.id);
};
const testClient = (data: any) => {
testClientMutation.mutate(data);
};
return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog
as="div"
static
className="fixed inset-0 overflow-hidden"
open={isOpen}
onClose={toggle}
initialFocus={cancelButtonRef}
>
<DeleteModal
isOpen={deleteModalIsOpen}
toggle={toggleDeleteModal}
buttonRef={cancelModalButtonRef}
deleteAction={deleteAction}
title="Remove download client"
text="Are you sure you want to remove this download client? This action cannot be undone."
/>
<div className="absolute inset-0 overflow-hidden">
<Dialog.Overlay className="absolute inset-0" />
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
<Transition.Child
as={Fragment}
enter="transform transition ease-in-out duration-500 sm:duration-700"
enterFrom="translate-x-full"
enterTo="translate-x-0"
leave="transform transition ease-in-out duration-500 sm:duration-700"
leaveFrom="translate-x-0"
leaveTo="translate-x-full"
>
<div className="w-screen max-w-2xl">
<Form
initialValues={{
id: client.id,
name: client.name,
type: client.type,
enabled: client.enabled,
host: client.host,
port: client.port,
ssl: client.ssl,
username: client.username,
password: client.password,
settings: client.settings,
}}
onSubmit={onSubmit}
>
{({ handleSubmit, values }) => {
return (
<form
className="h-full flex flex-col bg-white shadow-xl overflow-y-scroll"
onSubmit={handleSubmit}
>
<div className="flex-1">
<div className="px-4 py-6 bg-gray-50 sm:px-6">
<div className="flex items-start justify-between space-x-3">
<div className="space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900">
Edit client
</Dialog.Title>
<p className="text-sm text-gray-500">
Edit download client settings.
</p>
</div>
<div className="h-7 flex items-center">
<button
type="button"
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
onClick={toggle}
>
<span className="sr-only">Close panel</span>
<XIcon
className="h-6 w-6"
aria-hidden="true"
/>
</button>
</div>
</div>
</div>
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<TextFieldWide name="name" label="Name" />
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="enabled" label="Enabled" />
</div>
<RadioFieldsetWide
name="type"
legend="Type"
options={DownloadClientTypeOptions}
/>
<div>{componentMap[values.type]}</div>
</div>
</div>
{rulesComponentMap[values.type]}
<div className="flex-shrink-0 px-4 border-t border-gray-200 py-5 sm:px-6">
<div className="space-x-3 flex justify-between">
<button
type="button"
className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
onClick={toggleDeleteModal}
>
Remove
</button>
<div className="flex">
<button
type="button"
className={classNames(
isSuccessfulTest
? "text-green-500 border-green-500 bg-green-50"
: isErrorTest
? "text-red-500 border-red-500 bg-red-50"
: "border-gray-300 text-gray-700 bg-white hover:bg-gray-50 focus:border-rose-700 active:bg-rose-700",
isTesting ? "cursor-not-allowed" : "",
"mr-2 inline-flex items-center px-4 py-2 border font-medium rounded-md shadow-sm text-sm transition ease-in-out duration-150"
)}
disabled={isTesting}
onClick={() => testClient(values)}
>
{isTesting ? (
<svg
className="animate-spin h-5 w-5 text-green-500"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
) : isSuccessfulTest ? (
"OK!"
) : isErrorTest ? (
"ERROR"
) : (
"Test"
)}
</button>
<button
type="button"
className="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
onClick={toggle}
>
Cancel
</button>
<button
type="submit"
className="ml-4 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Save
</button>
</div>
</div>
</div>
<DEBUG values={values} />
</form>
);
}}
</Form>
</div>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
}
export default DownloadClientUpdateForm;

View file

@ -1,122 +0,0 @@
import { Fragment } from "react";
import { SwitchGroup, TextFieldWide } from "../../../components/inputs";
import { NumberFieldWide, PasswordFieldWide } from "../../../components/inputs/wide";
import { useField } from "react-final-form";
import { Dialog } from "@headlessui/react";
function FormFieldsDefault() {
return (
<Fragment>
<TextFieldWide name="host" label="Host" help="Url domain.ltd/client" />
<NumberFieldWide name="port" label="Port" />
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="ssl" label="SSL" />
</div>
<TextFieldWide name="username" label="Username" />
<PasswordFieldWide name="password" label="Password" />
</Fragment>
);
}
function FormFieldsArr() {
const { input } = useField("settings.basic.auth");
return (
<Fragment>
<TextFieldWide name="host" label="Host" help="Full url like http(s)://domain.ltd/" />
<PasswordFieldWide name="settings.apikey" label="API key" />
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="settings.basic.auth" label="Basic auth" />
</div>
{input.value === true && (
<Fragment>
<TextFieldWide name="settings.basic.username" label="Username" />
<PasswordFieldWide name="settings.basic.password" label="Password" />
</Fragment>
)}
</Fragment>
);
}
export const componentMap: any = {
DELUGE_V1: <FormFieldsDefault />,
DELUGE_V2: <FormFieldsDefault />,
QBITTORRENT: <FormFieldsDefault />,
RADARR: <FormFieldsArr />,
SONARR: <FormFieldsArr />,
LIDARR: <FormFieldsArr />,
};
function FormFieldsRulesBasic() {
const { input: enabled } = useField("settings.rules.enabled");
return (
<div className="border-t border-gray-200 py-5">
<div className="px-6 space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900">Rules</Dialog.Title>
<p className="text-sm text-gray-500">
Manage max downloads.
</p>
</div>
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="settings.rules.enabled" label="Enabled" />
</div>
{enabled.value === true && (
<Fragment>
<NumberFieldWide name="settings.rules.max_active_downloads" label="Max active downloads" />
</Fragment>
)}
</div>
);
}
function FormFieldsRules() {
const { input } = useField("settings.rules.ignore_slow_torrents");
const { input: enabled } = useField("settings.rules.enabled");
return (
<div className="border-t border-gray-200 py-5">
<div className="px-6 space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900">Rules</Dialog.Title>
<p className="text-sm text-gray-500">
Manage max downloads etc.
</p>
</div>
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="settings.rules.enabled" label="Enabled" />
</div>
{enabled.value === true && (
<Fragment>
<NumberFieldWide name="settings.rules.max_active_downloads" label="Max active downloads" />
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroup name="settings.rules.ignore_slow_torrents" label="Ignore slow torrents" />
</div>
{input.value === true && (
<Fragment>
<NumberFieldWide name="settings.rules.download_speed_threshold" label="Download speed threshold" placeholder="in KB/s" help="If download speed is below this when max active downloads is hit, download anyways. KB/s" />
</Fragment>
)}
</Fragment>
)}
</div>
);
}
export const rulesComponentMap: any = {
DELUGE_V1: <FormFieldsRulesBasic />,
DELUGE_V2: <FormFieldsRulesBasic />,
QBITTORRENT: <FormFieldsRules />,
};

View file

@ -17,7 +17,7 @@ export default function Base() {
return ( return (
<div className=""> <div className="">
<Disclosure as="nav" className="bg-gray-800 pb-48"> <Disclosure as="nav" className="bg-gray-900 pb-48">
{({ open }) => ( {({ open }) => (
<> <>
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
@ -40,11 +40,34 @@ export default function Base() {
<div className="flex items-baseline space-x-4"> <div className="flex items-baseline space-x-4">
{nav.map((item, itemIdx) => {nav.map((item, itemIdx) =>
<NavLink <NavLink
key={itemIdx} key={item.name + itemIdx}
to={item.path} to={item.path}
exact={true} strict={true}
activeClassName="bg-gray-900 text-white " isActive={(match, location) => {
className="text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium" if (match?.url === "/" && item.path === "/" && location.pathname === "/") {
return true
}
// if (item.path ==="/" && location.pathname ==="/") {
// console.log("match base");
// return true
// }
if (!match) {
return false;
}
if (match.url === "/") {
return false;
}
return true;
}}
activeClassName="bg-gray-900 dark:bg-gray-700 text-white "
className={classNames(
"text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium"
)}
> >
{item.name} {item.name}
</NavLink> </NavLink>
@ -59,7 +82,7 @@ export default function Base() {
<> <>
<div> <div>
<Menu.Button <Menu.Button
className="max-w-xs bg-gray-800 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white"> className="max-w-xs rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white">
<span <span
className="hidden text-gray-300 text-sm font-medium sm:block"> className="hidden text-gray-300 text-sm font-medium sm:block">
<span className="sr-only">Open user menu for </span>User <span className="sr-only">Open user menu for </span>User
@ -82,15 +105,15 @@ export default function Base() {
> >
<Menu.Items <Menu.Items
static static
className="origin-top-right absolute right-0 mt-2 w-48 z-10 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none" className="origin-top-right absolute right-0 mt-2 w-48 z-10 rounded-md shadow-lg py-1 bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 focus:outline-none"
> >
<Menu.Item> <Menu.Item>
{({ active }) => ( {({ active }) => (
<Link <Link
to="settings" to="settings"
className={classNames( className={classNames(
active ? 'bg-gray-100' : '', active ? 'bg-gray-100 dark:bg-gray-600' : '',
'block px-4 py-2 text-sm text-gray-700' 'block px-4 py-2 text-sm text-gray-700 dark:text-gray-200'
)} )}
> >
Settings Settings
@ -102,8 +125,8 @@ export default function Base() {
<Link <Link
to="/logout" to="/logout"
className={classNames( className={classNames(
active ? 'bg-gray-100' : '', active ? 'bg-gray-100 dark:bg-gray-600' : '',
'block px-4 py-2 text-sm text-gray-700' 'block px-4 py-2 text-sm text-gray-700 dark:text-gray-200'
)} )}
> >
Logout Logout
@ -138,7 +161,6 @@ export default function Base() {
{nav.map((item, itemIdx) => {nav.map((item, itemIdx) =>
itemIdx === 0 ? ( itemIdx === 0 ? (
<Fragment key={item.path}> <Fragment key={item.path}>
{/* Current: "bg-gray-900 text-white", Default: "text-gray-300 hover:bg-gray-700 hover:text-white" */}
<Link to={item.path} <Link to={item.path}
className="bg-gray-900 text-white block px-3 py-2 rounded-md text-base font-medium"> className="bg-gray-900 text-white block px-3 py-2 rounded-md text-base font-medium">
{item.name} {item.name}

View file

@ -7,12 +7,10 @@ export function Dashboard() {
</div> </div>
</header> </header>
<div className="max-w-7xl mx-auto pb-12 px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto pb-12 px-4 sm:px-6 lg:px-8">
<div className="bg-white rounded-lg shadow px-5 py-6 sm:px-6"> <div className="bg-white dark:bg-gray-800 rounded-lg shadow px-5 py-6 sm:px-6">
<div className="border-4 border-dashed border-gray-200 rounded-lg h-96" /> <div className="border-4 border-dashed border-gray-200 dark:border-gray-700 rounded-lg h-96" />
</div> </div>
</div> </div>
</main> </main>
) )
} }

View file

@ -38,7 +38,7 @@ export default function Logs() {
</div> </div>
</header> </header>
<div className="max-w-7xl mx-auto pb-12 px-2 sm:px-4 lg:px-8"> <div className="max-w-7xl mx-auto pb-12 px-2 sm:px-4 lg:px-8">
<div className="bg-white rounded-lg shadow px-2 sm:px-4 py-3 sm:py-4"> <div className="bg-white dark:bg-gray-800 rounded-lg shadow px-2 sm:px-4 py-3 sm:py-4">
<div className=" overflow-y-auto p-2 rounded-lg h-96 bg-gray-900"> <div className=" overflow-y-auto p-2 rounded-lg h-96 bg-gray-900">
{logs.map((a, idx) => ( {logs.map((a, idx) => (
<p key={idx}> <p key={idx}>

View file

@ -1,4 +1,3 @@
import React from 'react'
import {CogIcon, DownloadIcon, KeyIcon} from '@heroicons/react/outline' import {CogIcon, DownloadIcon, KeyIcon} from '@heroicons/react/outline'
import {NavLink, Route, Switch as RouteSwitch, useLocation, useRouteMatch} from "react-router-dom"; import {NavLink, Route, Switch as RouteSwitch, useLocation, useRouteMatch} from "react-router-dom";
import IndexerSettings from "./settings/Indexer"; import IndexerSettings from "./settings/Indexer";
@ -33,17 +32,17 @@ function SubNavLink({item, url}: any) {
key={item.name} key={item.name}
to={too} to={too}
exact={true} exact={true}
activeClassName="bg-teal-50 border-teal-500 text-teal-700 hover:bg-teal-50 hover:text-teal-700" activeClassName="bg-teal-50 dark:bg-gray-700 border-teal-500 dark:border-blue-500 text-teal-700 dark:text-white hover:bg-teal-50 dark:hover:bg-gray-500 hover:text-teal-700 dark:hover:text-gray-200"
className={classNames( className={classNames(
'border-transparent text-gray-900 hover:bg-gray-50 hover:text-gray-900 group border-l-4 px-3 py-2 flex items-center text-sm font-medium' 'border-transparent text-gray-900 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-300 group border-l-4 px-3 py-2 flex items-center text-sm font-medium'
)} )}
aria-current={splitLocation[2] === item.href ? 'page' : undefined} aria-current={splitLocation[2] === item.href ? 'page' : undefined}
> >
<item.icon <item.icon
className={classNames( className={classNames(
splitLocation[2] === item.href splitLocation[2] === item.href
? 'text-teal-500 group-hover:text-teal-500' ? 'text-teal-500 dark:text-blue-600 group-hover:text-teal-500 dark:group-hover:text-blue-600'
: 'text-gray-400 group-hover:text-gray-500', : 'text-gray-400 group-hover:text-gray-500 dark:group-hover:text-gray-300',
'flex-shrink-0 -ml-1 mr-3 h-6 w-6' 'flex-shrink-0 -ml-1 mr-3 h-6 w-6'
)} )}
aria-hidden="true" aria-hidden="true"
@ -77,8 +76,8 @@ export default function Settings() {
</header> </header>
<div className="max-w-screen-xl mx-auto pb-6 px-4 sm:px-6 lg:pb-16 lg:px-8"> <div className="max-w-screen-xl mx-auto pb-6 px-4 sm:px-6 lg:pb-16 lg:px-8">
<div className="bg-white rounded-lg shadow overflow-hidden"> <div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
<div className="divide-y divide-gray-200 lg:grid lg:grid-cols-12 lg:divide-y-0 lg:divide-x"> <div className="divide-y divide-gray-200 dark:divide-gray-700 lg:grid lg:grid-cols-12 lg:divide-y-0 lg:divide-x">
<SidebarNav url={url} subNavigation={subNavigation}/> <SidebarNav url={url} subNavigation={subNavigation}/>
<RouteSwitch> <RouteSwitch>

View file

@ -1,11 +1,11 @@
import {useMutation} from "react-query"; import { useMutation } from "react-query";
import APIClient from "../../api/APIClient"; import APIClient from "../../api/APIClient";
import {Form} from "react-final-form"; import { Form } from "react-final-form";
import {PasswordField, TextField} from "../../components/inputs"; import { PasswordField, TextField } from "../../components/inputs";
import {useRecoilState} from "recoil"; import { useRecoilState } from "recoil";
import {isLoggedIn} from "../../state/state"; import { isLoggedIn } from "../../state/state";
import {useHistory} from "react-router-dom"; import { useHistory } from "react-router-dom";
import {useEffect} from "react"; import { useEffect } from "react";
import logo from "../../logo.png" import logo from "../../logo.png"
interface loginData { interface loginData {
@ -18,7 +18,7 @@ function Login() {
let history = useHistory(); let history = useHistory();
useEffect(() => { useEffect(() => {
if(loggedIn) { if (loggedIn) {
// setLoading(false); // setLoading(false);
history.push('/'); history.push('/');
} else { } else {
@ -38,7 +38,7 @@ function Login() {
} }
return ( return (
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8"> <div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md mb-6"> <div className="sm:mx-auto sm:w-full sm:max-w-md mb-6">
<img <img
className="mx-auto h-12 w-auto" className="mx-auto h-12 w-auto"
@ -48,7 +48,7 @@ function Login() {
</div> </div>
<div className="sm:mx-auto sm:w-full sm:max-w-md"> <div className="sm:mx-auto sm:w-full sm:max-w-md">
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10"> <div className="bg-white dark:bg-gray-800 py-8 px-4 shadow sm:rounded-lg sm:px-10">
<Form <Form
initialValues={{ initialValues={{
@ -57,11 +57,11 @@ function Login() {
}} }}
onSubmit={onSubmit} onSubmit={onSubmit}
> >
{({handleSubmit, values}) => { {({ handleSubmit, values }) => {
return ( return (
<form className="space-y-6" onSubmit={handleSubmit}> <form className="space-y-6" onSubmit={handleSubmit}>
<TextField name="username" label="Username" autoComplete="username" /> <TextField name="username" label="Username" autoComplete="username" />
<PasswordField name="password" label="password" autoComplete="current-password"/> <PasswordField name="password" label="password" autoComplete="current-password" />
{/*<div className="flex items-center justify-between">*/} {/*<div className="flex items-center justify-between">*/}
{/* <div className="flex items-center">*/} {/* <div className="flex items-center">*/}
@ -86,7 +86,7 @@ function Login() {
<div> <div>
<button <button
type="submit" type="submit"
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
> >
Sign in Sign in
</button> </button>

View file

@ -9,18 +9,18 @@ function Logout() {
const [loggedIn, setLoggedIn] = useRecoilState(isLoggedIn); const [loggedIn, setLoggedIn] = useRecoilState(isLoggedIn);
let history = useHistory(); let history = useHistory();
const [_, removeCookie] = useCookies(['user_session']); const [,, removeCookie] = useCookies(['user_session']);
useEffect(() => { useEffect(() => {
APIClient.auth.logout().then(r => { APIClient.auth.logout().then(r => {
removeCookie("user_session", "") removeCookie("user_session")
setLoggedIn(false); setLoggedIn(false);
history.push('/login'); history.push('/login');
}) })
}, [loggedIn, history, removeCookie, setLoggedIn]) }, [loggedIn, history, removeCookie, setLoggedIn])
return ( return (
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8"> <div className="min-h-screen bg-gray-50 dark:bg-gray-800 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<p>Logged out</p> <p>Logged out</p>
</div> </div>
) )

View file

@ -1,4 +1,4 @@
import React, { Fragment, useRef } from "react"; import { Fragment, useRef } from "react";
import { Dialog, Transition, Switch as SwitchBasic } from "@headlessui/react"; import { Dialog, Transition, Switch as SwitchBasic } from "@headlessui/react";
import { ChevronDownIcon, ChevronRightIcon, ExclamationIcon, } from '@heroicons/react/solid' import { ChevronDownIcon, ChevronRightIcon, ExclamationIcon, } from '@heroicons/react/solid'
import { EmptyListState } from "../../components/EmptyListState"; import { EmptyListState } from "../../components/EmptyListState";
@ -55,9 +55,9 @@ function TabNavLink({ item, url }: any) {
key={item.name} key={item.name}
to={too} to={too}
exact={true} exact={true}
activeClassName="border-purple-600 text-purple-600" activeClassName="border-purple-600 dark:border-blue-500 text-purple-600 dark:text-white"
className={classNames( className={classNames(
'border-transparent text-gray-500 hover:text-purple-600 hover:border-purple-600 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm' 'border-transparent text-gray-500 hover:text-purple-600 dark:hover:text-white hover:border-purple-600 dark:hover:border-blue-500 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm'
)} )}
aria-current={splitLocation[2] === item.href ? 'page' : undefined} aria-current={splitLocation[2] === item.href ? 'page' : undefined}
> >
@ -72,7 +72,7 @@ const FormButtonsGroup = ({ deleteAction, reset, dirty }: any) => {
const cancelButtonRef = useRef(null) const cancelButtonRef = useRef(null)
return ( return (
<div className="pt-6 divide-y divide-gray-200"> <div className="pt-6 divide-y divide-gray-200 dark:divide-gray-700">
<Transition.Root show={deleteModalIsOpen} as={Fragment}> <Transition.Root show={deleteModalIsOpen} as={Fragment}>
<Dialog <Dialog
@ -97,7 +97,6 @@ const FormButtonsGroup = ({ deleteAction, reset, dirty }: any) => {
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" /> <Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child> </Transition.Child>
{/* This element is to trick the browser into centering the modal contents. */}
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true"> <span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203; &#8203;
</span> </span>
@ -142,7 +141,7 @@ const FormButtonsGroup = ({ deleteAction, reset, dirty }: any) => {
</button> </button>
<button <button
type="button" type="button"
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm" className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 light:bg-white text-base font-medium text-gray-700 dark:text-red-500 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
onClick={toggleDeleteModal} onClick={toggleDeleteModal}
ref={cancelButtonRef} ref={cancelButtonRef}
> >
@ -158,7 +157,7 @@ const FormButtonsGroup = ({ deleteAction, reset, dirty }: any) => {
<div className="mt-4 pt-4 flex justify-between"> <div className="mt-4 pt-4 flex justify-between">
<button <button
type="button" type="button"
className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm" className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 dark:text-red-500 light:bg-red-100 light:hover:bg-red-200 dark:hover:text-red-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
onClick={toggleDeleteModal} onClick={toggleDeleteModal}
> >
Remove Remove
@ -168,14 +167,14 @@ const FormButtonsGroup = ({ deleteAction, reset, dirty }: any) => {
{/* {dirty && <span className="mr-4 text-sm text-gray-500">Unsaved changes..</span>} */} {/* {dirty && <span className="mr-4 text-sm text-gray-500">Unsaved changes..</span>} */}
<button <button
type="button" type="button"
className="bg-white border border-gray-300 rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500" className="light:bg-white light:border light:border-gray-300 rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-gray-700 dark:text-gray-500 light:hover:bg-gray-50 dark:hover:text-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
onClick={reset} onClick={reset}
> >
Cancel Cancel
</button> </button>
<button <button
type="submit" type="submit"
className="ml-4 relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" className="ml-4 relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
> >
Save Save
</button> </button>
@ -266,7 +265,7 @@ export default function FilterDetails() {
</div> </div>
</header> </header>
<div className="max-w-7xl mx-auto pb-12 px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto pb-12 px-4 sm:px-6 lg:px-8">
<div className="bg-white rounded-lg shadow"> <div className="bg-white dark:bg-gray-800 rounded-lg shadow">
<div className="relative mx-auto md:px-6 xl:px-4"> <div className="relative mx-auto md:px-6 xl:px-4">
<div className="px-4 sm:px-6 md:px-0"> <div className="px-4 sm:px-6 md:px-0">
<div className="pt-2 pb-6"> <div className="pt-2 pb-6">
@ -278,7 +277,7 @@ export default function FilterDetails() {
<select <select
id="selected-tab" id="selected-tab"
name="selected-tab" name="selected-tab"
className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-purple-500 focus:border-purple-500 sm:text-sm rounded-md" className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-700 focus:outline-none focus:ring-purple-500 focus:border-purple-500 sm:text-sm rounded-md"
> >
{tabs.map((tab) => ( {tabs.map((tab) => (
<option key={tab.name} onClick={(e) => handleMobileNav(e, tab.href)}> <option key={tab.name} onClick={(e) => handleMobileNav(e, tab.href)}>
@ -288,7 +287,7 @@ export default function FilterDetails() {
</select> </select>
</div> </div>
<div className="hidden sm:block"> <div className="hidden sm:block">
<div className="border-b border-gray-200"> <div className="border-b border-gray-200 dark:border-gray-700">
<nav className="-mb-px flex space-x-8"> <nav className="-mb-px flex space-x-8">
{tabs.map((tab) => ( {tabs.map((tab) => (
<TabNavLink item={tab} url={url} key={tab.href} /> <TabNavLink item={tab} url={url} key={tab.href} />
@ -385,7 +384,7 @@ function General({ indexers }: GeneralProps) {
<TextField name="name" label="Filter name" columns={6} placeholder="eg. Filter 1" /> <TextField name="name" label="Filter name" columns={6} placeholder="eg. Filter 1" />
<div className="col-span-6"> <div className="col-span-6">
<label htmlFor="indexers" className="block text-xs font-bold text-gray-700 uppercase tracking-wide"> <label htmlFor="indexers" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
Indexers Indexers
</label> </label>
@ -409,7 +408,7 @@ function General({ indexers }: GeneralProps) {
isClearable={true} isClearable={true}
isMulti={true} isMulti={true}
placeholder="Choose indexers" placeholder="Choose indexers"
className="mt-2 block w-full focus:outline-none focus:ring-light-blue-500 focus:border-light-blue-500 sm:text-sm" className="mt-2 block w-full focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
options={opts} options={opts}
/> />
) )
@ -429,7 +428,7 @@ function General({ indexers }: GeneralProps) {
</div> </div>
</div> </div>
<div className="border-t"> <div className="border-t dark:border-gray-700">
<SwitchGroup name="enabled" label="Enabled" description="Enabled or disable filter." /> <SwitchGroup name="enabled" label="Enabled" description="Enabled or disable filter." />
</div> </div>
@ -485,11 +484,11 @@ function Advanced() {
return ( return (
<div> <div>
<div className="mt-6 lg:pb-8 border-b border-gray-200"> <div className="mt-6 lg:pb-8 border-b border-gray-200 dark:border-gray-700">
<div className="flex justify-between items-center cursor-pointer" onClick={toggleReleases}> <div className="flex justify-between items-center cursor-pointer" onClick={toggleReleases}>
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline"> <div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900">Releases</h3> <h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900 dark:text-gray-200">Releases</h3>
<p className="ml-2 mt-1 text-sm text-gray-500 truncate">Match or ignore</p> <p className="ml-2 mt-1 text-sm text-gray-500 dark:text-gray-400 truncate">Match or ignore</p>
</div> </div>
<div className="mt-3 sm:mt-0 sm:ml-4"> <div className="mt-3 sm:mt-0 sm:ml-4">
<button <button
@ -508,11 +507,11 @@ function Advanced() {
)} )}
</div> </div>
<div className="mt-6 lg:pb-8 border-b border-gray-200"> <div className="mt-6 lg:pb-8 border-b border-gray-200 dark:border-gray-700">
<div className="flex justify-between items-center cursor-pointer" onClick={toggleGroups}> <div className="flex justify-between items-center cursor-pointer" onClick={toggleGroups}>
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline"> <div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900">Groups</h3> <h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900 dark:text-gray-200">Groups</h3>
<p className="ml-2 mt-1 text-sm text-gray-500 truncate">Match or ignore</p> <p className="ml-2 mt-1 text-sm text-gray-500 dark:text-gray-400 truncate">Match or ignore</p>
</div> </div>
<div className="mt-3 sm:mt-0 sm:ml-4"> <div className="mt-3 sm:mt-0 sm:ml-4">
<button <button
@ -531,11 +530,11 @@ function Advanced() {
)} )}
</div> </div>
<div className="mt-6 lg:pb-8 border-b border-gray-200"> <div className="mt-6 lg:pb-8 border-b border-gray-200 dark:border-gray-700">
<div className="flex justify-between items-center cursor-pointer" onClick={toggleCategories}> <div className="flex justify-between items-center cursor-pointer" onClick={toggleCategories}>
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline"> <div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900">Categories and tags</h3> <h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900 dark:text-gray-200">Categories and tags</h3>
<p className="ml-2 mt-1 text-sm text-gray-500 truncate">Match or ignore categories or tags</p> <p className="ml-2 mt-1 text-sm text-gray-500 dark:text-gray-400 truncate">Match or ignore categories or tags</p>
</div> </div>
<div className="mt-3 sm:mt-0 sm:ml-4"> <div className="mt-3 sm:mt-0 sm:ml-4">
<button <button
@ -557,11 +556,11 @@ function Advanced() {
)} )}
</div> </div>
<div className="mt-6 lg:pb-8 border-b border-gray-200"> <div className="mt-6 lg:pb-8 border-b border-gray-200 dark:border-gray-700">
<div className="flex justify-between items-center cursor-pointer" onClick={toggleUploaders}> <div className="flex justify-between items-center cursor-pointer" onClick={toggleUploaders}>
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline"> <div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900">Uploaders</h3> <h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900 dark:text-gray-200">Uploaders</h3>
<p className="ml-2 mt-1 text-sm text-gray-500 truncate">Match or ignore uploaders</p> <p className="ml-2 mt-1 text-sm text-gray-500 dark:text-gray-400 truncate">Match or ignore uploaders</p>
</div> </div>
<div className="mt-3 sm:mt-0 sm:ml-4"> <div className="mt-3 sm:mt-0 sm:ml-4">
<button <button
@ -580,11 +579,11 @@ function Advanced() {
)} )}
</div> </div>
<div className="mt-6 lg:pb-8 border-b border-gray-200"> <div className="mt-6 lg:pb-8 border-b border-gray-200 dark:border-gray-700">
<div className="flex justify-between items-center cursor-pointer" onClick={toggleFreeleech}> <div className="flex justify-between items-center cursor-pointer" onClick={toggleFreeleech}>
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline"> <div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900">Freeleech</h3> <h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900 dark:text-gray-200">Freeleech</h3>
<p className="ml-2 mt-1 text-sm text-gray-500 truncate">Match only freeleech and freeleech percent</p> <p className="ml-2 mt-1 text-sm text-gray-500 dark:text-gray-400 truncate">Match only freeleech and freeleech percent</p>
</div> </div>
<div className="mt-3 sm:mt-0 sm:ml-4"> <div className="mt-3 sm:mt-0 sm:ml-4">
<button <button
@ -615,8 +614,6 @@ interface FilterActionsProps {
} }
function FilterActions({ filter, values }: FilterActionsProps) { function FilterActions({ filter, values }: FilterActionsProps) {
// const [addActionIsOpen, toggleAddAction] = useToggle(false)
const { data } = useQuery<DownloadClient[], Error>('downloadClients', APIClient.download_clients.getAll, const { data } = useQuery<DownloadClient[], Error>('downloadClients', APIClient.download_clients.getAll,
{ {
refetchOnWindowFocus: false refetchOnWindowFocus: false
@ -644,36 +641,35 @@ function FilterActions({ filter, values }: FilterActionsProps) {
return ( return (
<div className="mt-10"> <div className="mt-10">
{/* {addActionIsOpen &&
<FilterActionAddForm filter={filter} clients={data || []} isOpen={addActionIsOpen} toggle={toggleAddAction} />
} */}
<FieldArray name="actions"> <FieldArray name="actions">
{({ remove, push }) => ( {({ remove, push }) => (
<Fragment> <Fragment>
<div className="-ml-4 -mt-4 mb-6 flex justify-between items-center flex-wrap sm:flex-nowrap"> <div className="-ml-4 -mt-4 mb-6 flex justify-between items-center flex-wrap sm:flex-nowrap">
<div className="ml-4 mt-4"> <div className="ml-4 mt-4">
<h3 className="text-lg leading-6 font-medium text-gray-900">Actions</h3> <h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-gray-200">Actions</h3>
<p className="mt-1 text-sm text-gray-500"> <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Add to download clients or run custom commands. Add to download clients or run custom commands.
</p> </p>
</div> </div>
<div className="ml-4 mt-4 flex-shrink-0"> <div className="ml-4 mt-4 flex-shrink-0">
<button <button
type="button" type="button"
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
onClick={() => push(newAction)} onClick={() => push(newAction)}
> >
Add new Add new
</button> </button>
</div> </div>
</div> </div>
<div className="bg-white shadow sm:rounded-md">
<div className="light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
{values.actions.length > 0 ? {values.actions.length > 0 ?
values.actions.map((action: any, index: any) => ( <ul className="divide-y divide-gray-200 dark:divide-gray-700">
<ul className="divide-y divide-gray-200" key={index}> {values.actions.map((action: any, index: any) => (
<FilterActionsItem action={action} clients={data!} idx={index} remove={remove} /> <FilterActionsItem action={action} clients={data!} idx={index} remove={remove} />
</ul> ))}
)) : <EmptyListState text="No actions yet!" /> </ul>
: <EmptyListState text="No actions yet!" />
} }
</div> </div>
</Fragment> </Fragment>
@ -867,8 +863,8 @@ function FilterActionsItem({ action, clients, idx, remove }: FilterActionsItemPr
<li> <li>
<div <div
className={classNames( className={classNames(
idx % 2 === 0 ? "bg-white" : "bg-gray-50", idx % 2 === 0 ? "bg-white dark:bg-gray-800" : "bg-gray-50 dark:bg-gray-700",
"flex items-center sm:px-6 hover:bg-gray-50" "flex items-center sm:px-6 hover:bg-gray-50 dark:hover:bg-gray-600"
)} )}
> >
<Field name={`actions.${idx}.enabled`} type="checkbox"> <Field name={`actions.${idx}.enabled`} type="checkbox">
@ -885,8 +881,8 @@ function FilterActionsItem({ action, clients, idx, remove }: FilterActionsItemPr
setFieldValue(field?.name ?? '', value) setFieldValue(field?.name ?? '', value)
}} }}
className={classNames( className={classNames(
field.value ? 'bg-teal-500' : 'bg-gray-200', field.value ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600',
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500' 'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
)} )}
> >
<span className="sr-only">toggle enabled</span> <span className="sr-only">toggle enabled</span>
@ -905,14 +901,14 @@ function FilterActionsItem({ action, clients, idx, remove }: FilterActionsItemPr
<div className="min-w-0 flex-1 sm:flex sm:items-center sm:justify-between"> <div className="min-w-0 flex-1 sm:flex sm:items-center sm:justify-between">
<div className="truncate"> <div className="truncate">
<div className="flex text-sm"> <div className="flex text-sm">
<p className="ml-4 font-medium text-indigo-600 truncate"> <p className="ml-4 font-medium text-indigo-600 dark:text-gray-100 truncate">
{action.name} {action.name}
</p> </p>
</div> </div>
</div> </div>
<div className="mt-4 flex-shrink-0 sm:mt-0 sm:ml-5"> <div className="mt-4 flex-shrink-0 sm:mt-0 sm:ml-5">
<div className="flex overflow-hidden -space-x-1"> <div className="flex overflow-hidden -space-x-1">
<span className="text-sm font-normal text-gray-500"> <span className="text-sm font-normal text-gray-500 dark:text-gray-400">
{ActionTypeNameMap[action.type]} {ActionTypeNameMap[action.type]}
</span> </span>
</div> </div>
@ -928,7 +924,7 @@ function FilterActionsItem({ action, clients, idx, remove }: FilterActionsItemPr
</div> </div>
{edit && ( {edit && (
<div className="px-4 py-4 flex items-center sm:px-6"> <div className="px-4 py-4 flex items-center sm:px-6 border dark:border-gray-600">
<Transition.Root show={deleteModalIsOpen} as={Fragment}> <Transition.Root show={deleteModalIsOpen} as={Fragment}>
<Dialog <Dialog
as="div" as="div"
@ -968,7 +964,7 @@ function FilterActionsItem({ action, clients, idx, remove }: FilterActionsItemPr
<div className="mt-4 pt-4 flex justify-between"> <div className="mt-4 pt-4 flex justify-between">
<button <button
type="button" type="button"
className="inline-flex items-center justify-center py-2 border border-transparent font-medium rounded-md text-red-700 hover:text-red-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm" className="inline-flex items-center justify-center py-2 border border-transparent font-medium rounded-md text-red-700 dark:text-red-500 hover:text-red-500 dark:hover:text-red-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
onClick={toggleDeleteModal} onClick={toggleDeleteModal}
> >
Remove Remove
@ -977,7 +973,7 @@ function FilterActionsItem({ action, clients, idx, remove }: FilterActionsItemPr
<div> <div>
<button <button
type="button" type="button"
className="bg-white border border-gray-300 rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500" className="light:bg-white light:border light:border-gray-300 rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-gray-700 dark:text-gray-500 light:hover:bg-gray-50 dark:hover:text-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
onClick={toggleEdit} onClick={toggleEdit}
> >
Close Close

View file

@ -1,4 +1,4 @@
import React, { Fragment } from "react"; import { Fragment } from "react";
import { Transition, Listbox } from "@headlessui/react"; import { Transition, Listbox } from "@headlessui/react";
import { CheckIcon, SelectorIcon } from '@heroicons/react/solid'; import { CheckIcon, SelectorIcon } from '@heroicons/react/solid';
import { Action, DownloadClient } from "../../../domain/interfaces"; import { Action, DownloadClient } from "../../../domain/interfaces";
@ -21,17 +21,17 @@ export default function DownloadClientSelect({
field, field,
form: { setFieldValue }, form: { setFieldValue },
}: any) => ( }: any) => (
<Listbox <Listbox
value={field.value} value={field.value}
onChange={(value: any) => setFieldValue(field?.name, value)} onChange={(value: any) => setFieldValue(field?.name, value)}
> >
{({ open }) => ( {({ open }) => (
<> <>
<Listbox.Label className="block text-xs font-bold text-gray-700 uppercase tracking-wide"> <Listbox.Label className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
Client Client
</Listbox.Label> </Listbox.Label>
<div className="mt-2 relative"> <div className="mt-2 relative">
<Listbox.Button className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"> <Listbox.Button className="bg-white dark:bg-gray-800 relative w-full border border-gray-300 dark:border-gray-700 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 dark:text-gray-200 sm:text-sm">
<span className="block truncate"> <span className="block truncate">
{field.value {field.value
? clients.find((c) => c.id === field.value)!.name ? clients.find((c) => c.id === field.value)!.name
@ -40,7 +40,7 @@ export default function DownloadClientSelect({
{/*<span className="block truncate">Choose a client</span>*/} {/*<span className="block truncate">Choose a client</span>*/}
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none"> <span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<SelectorIcon <SelectorIcon
className="h-5 w-5 text-gray-400" className="h-5 w-5 text-gray-400 dark:text-gray-300"
aria-hidden="true" /> aria-hidden="true" />
</span> </span>
</Listbox.Button> </Listbox.Button>
@ -54,7 +54,7 @@ export default function DownloadClientSelect({
> >
<Listbox.Options <Listbox.Options
static static
className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" className="absolute z-10 mt-1 w-full bg-white dark:bg-gray-800 shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
> >
{clients {clients
.filter((c) => c.type === action.type) .filter((c) => c.type === action.type)
@ -63,8 +63,8 @@ export default function DownloadClientSelect({
key={client.id} key={client.id}
className={({ active }) => classNames( className={({ active }) => classNames(
active active
? "text-white bg-indigo-600" ? "text-white dark:text-gray-100 bg-indigo-600 dark:bg-gray-800"
: "text-gray-900", : "text-gray-900 dark:text-gray-300",
"cursor-default select-none relative py-2 pl-3 pr-9" "cursor-default select-none relative py-2 pl-3 pr-9"
)} )}
value={client.id} value={client.id}
@ -83,7 +83,7 @@ export default function DownloadClientSelect({
{selected ? ( {selected ? (
<span <span
className={classNames( className={classNames(
active ? "text-white" : "text-indigo-600", active ? "text-white dark:text-gray-100" : "text-indigo-600 dark:text-gray-700",
"absolute inset-y-0 right-0 flex items-center pr-4" "absolute inset-y-0 right-0 flex items-center pr-4"
)} )}
> >

View file

@ -24,7 +24,7 @@ const MultiSelect: React.FC<Props> = ({
)} )}
> >
<label <label
className="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2" className="block uppercase tracking-wide text-gray-700 dark:text-gray-200 text-xs font-bold mb-2"
htmlFor={label} htmlFor={label}
> >
{label} {label}
@ -34,22 +34,21 @@ const MultiSelect: React.FC<Props> = ({
{({ {({
field, field,
form: { setFieldValue }, form: { setFieldValue },
}: any) => { }: any) => (
return ( <RMSC
<RMSC {...field}
{...field} type="select"
type="select" options={options}
options={options} labelledBy={name}
labelledBy={name} value={field.value && field.value.map((item: any) => options.find((o: any) => o.value === item))}
value={field.value && field.value.map((item: any) => options.find((o: any) => o.value === item))} onChange={(values: any) => {
onChange={(values: any) => { let am = values && values.map((i: any) => i.value)
let am = values && values.map((i: any) => i.value)
setFieldValue(field.name, am) setFieldValue(field.name, am)
}} }}
/> className="dark:bg-gray-700"
) />
}} )}
</Field> </Field>
</div> </div>
); );

View file

@ -1,5 +1,5 @@
import { Field } from "formik";
import React from "react"; import React from "react";
import { Field } from "formik";
import { classNames } from "../../../styles/utils"; import { classNames } from "../../../styles/utils";
interface Props { interface Props {
@ -18,14 +18,13 @@ const NumberField: React.FC<Props> = ({
className, className,
}) => ( }) => (
<div className="col-span-12 sm:col-span-6"> <div className="col-span-12 sm:col-span-6">
<label htmlFor={name} className="block text-sm font-medium text-gray-700"> <label htmlFor={name} className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
{label} {label}
</label> </label>
<Field name={name} type="number"> <Field name={name} type="number">
{({ {({
field, field,
form: { touched, errors },
meta, meta,
}: any) => ( }: any) => (
<div className="sm:col-span-2"> <div className="sm:col-span-2">
@ -35,8 +34,8 @@ const NumberField: React.FC<Props> = ({
className={classNames( className={classNames(
meta.touched && meta.error meta.touched && meta.error
? "focus:ring-red-500 focus:border-red-500 border-red-500" ? "focus:ring-red-500 focus:border-red-500 border-red-500"
: "focus:ring-indigo-500 focus:border-indigo-500 border-gray-300", : "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300",
"block w-full shadow-sm sm:text-sm rounded-md" "mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 shadow-sm dark:text-gray-100 sm:text-sm rounded-md"
)} )}
placeholder={placeholder} placeholder={placeholder}
/> />

View file

@ -30,11 +30,11 @@ function Select({ name, label, optionDefaultText, options }: props) {
> >
{({ open }) => ( {({ open }) => (
<> <>
<Listbox.Label className="block text-xs font-bold text-gray-700 uppercase tracking-wide"> <Listbox.Label className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
{label} {label}
</Listbox.Label> </Listbox.Label>
<div className="mt-2 relative"> <div className="mt-2 relative">
<Listbox.Button className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"> <Listbox.Button className="bg-white dark:bg-gray-800 relative w-full border border-gray-300 dark:border-gray-700 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 dark:text-gray-200 sm:text-sm">
<span className="block truncate"> <span className="block truncate">
{field.value {field.value
? options.find((c) => c.value === field.value)!.label ? options.find((c) => c.value === field.value)!.label
@ -43,7 +43,7 @@ function Select({ name, label, optionDefaultText, options }: props) {
</span> </span>
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none"> <span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<SelectorIcon <SelectorIcon
className="h-5 w-5 text-gray-400" className="h-5 w-5 text-gray-400 dark:text-gray-300"
aria-hidden="true" aria-hidden="true"
/> />
</span> </span>
@ -58,7 +58,7 @@ function Select({ name, label, optionDefaultText, options }: props) {
> >
<Listbox.Options <Listbox.Options
static static
className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" className="absolute z-10 mt-1 w-full bg-white dark:bg-gray-800 shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
> >
{options.map((opt) => ( {options.map((opt) => (
<Listbox.Option <Listbox.Option
@ -66,8 +66,8 @@ function Select({ name, label, optionDefaultText, options }: props) {
className={({ active }) => className={({ active }) =>
classNames( classNames(
active active
? "text-white bg-indigo-600" ? "text-white dark:text-gray-100 bg-indigo-600 dark:bg-gray-800"
: "text-gray-900", : "text-gray-900 dark:text-gray-300",
"cursor-default select-none relative py-2 pl-3 pr-9" "cursor-default select-none relative py-2 pl-3 pr-9"
) )
} }
@ -87,7 +87,7 @@ function Select({ name, label, optionDefaultText, options }: props) {
{selected ? ( {selected ? (
<span <span
className={classNames( className={classNames(
active ? "text-white" : "text-indigo-600", active ? "text-white dark:text-gray-100" : "text-indigo-600 dark:text-gray-700",
"absolute inset-y-0 right-0 flex items-center pr-4" "absolute inset-y-0 right-0 flex items-center pr-4"
)} )}
> >

View file

@ -1,6 +1,6 @@
import React, { InputHTMLAttributes } from 'react'
import { Switch as HeadlessSwitch } from '@headlessui/react' import { Switch as HeadlessSwitch } from '@headlessui/react'
import { FieldInputProps, FieldMetaProps, FieldProps, FormikProps, FormikValues } from 'formik' import { FieldInputProps, FieldMetaProps, FieldProps, FormikProps, FormikValues } from 'formik'
import React, { InputHTMLAttributes } from 'react'
import { classNames } from "../../../styles/utils"; import { classNames } from "../../../styles/utils";
type SwitchProps<V = any> = { type SwitchProps<V = any> = {
@ -37,8 +37,8 @@ export const Switch: React.FC<SwitchProps> = ({
}} }}
className={classNames( className={classNames(
checked ? 'bg-teal-500' : 'bg-gray-200', checked ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600',
'ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500' 'ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
)} )}
> >
{({ checked }) => ( {({ checked }) => (

View file

@ -15,12 +15,12 @@ const SwitchGroup: React.FC<Props> = ({ name, label, description, defaultValue }
<ul className="mt-2 divide-y divide-gray-200"> <ul className="mt-2 divide-y divide-gray-200">
<Switch.Group as="li" className="py-4 flex items-center justify-between"> <Switch.Group as="li" className="py-4 flex items-center justify-between">
{label && <div className="flex flex-col"> {label && <div className="flex flex-col">
<Switch.Label as="p" className="text-sm font-medium text-gray-900" <Switch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-gray-100"
passive> passive>
{label} {label}
</Switch.Label> </Switch.Label>
{description && ( {description && (
<Switch.Description className="text-sm text-gray-500"> <Switch.Description className="text-sm text-gray-500 dark:text-gray-400">
{description} {description}
</Switch.Description> </Switch.Description>
)} )}
@ -41,8 +41,8 @@ const SwitchGroup: React.FC<Props> = ({ name, label, description, defaultValue }
setFieldValue(field?.name ?? '', value) setFieldValue(field?.name ?? '', value)
}} }}
className={classNames( className={classNames(
field.value ? 'bg-teal-500' : 'bg-gray-200', field.value ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200',
'ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500' 'ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
)} )}
> >
{/* <span className="sr-only">{label}</span> */} {/* <span className="sr-only">{label}</span> */}
@ -68,7 +68,7 @@ const SwitchGroup: React.FC<Props> = ({ name, label, description, defaultValue }
onChange={onChange} onChange={onChange}
className={classNames( className={classNames(
value ? 'bg-teal-500' : 'bg-gray-200', value ? 'bg-teal-500' : 'bg-gray-200',
'ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500' 'ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
)} )}
> >
<span className="sr-only">Use setting</span> <span className="sr-only">Use setting</span>

View file

@ -1,5 +1,5 @@
import { Field } from "formik";
import React from "react"; import React from "react";
import { Field } from "formik";
import { classNames } from "../../../styles/utils"; import { classNames } from "../../../styles/utils";
type COL_WIDTHS = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; type COL_WIDTHS = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
@ -20,7 +20,7 @@ const TextField: React.FC<Props> = ({ name, label, placeholder, columns, classNa
)} )}
> >
{label && ( {label && (
<label htmlFor={name} className="block text-xs font-bold text-gray-700 uppercase tracking-wide"> <label htmlFor={name} className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
{label} {label}
</label> </label>
)} )}
@ -35,7 +35,7 @@ const TextField: React.FC<Props> = ({ name, label, placeholder, columns, classNa
id={name} id={name}
type="text" type="text"
autoComplete={autoComplete} autoComplete={autoComplete}
className="mt-2 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-light-blue-500 focus:border-light-blue-500 sm:text-sm" className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
placeholder={placeholder} placeholder={placeholder}
/> />

View file

@ -1,4 +1,4 @@
import React, { useState } from "react"; import { useState } from "react";
import { Switch } from "@headlessui/react"; import { Switch } from "@headlessui/react";
import { EmptyListState } from "../../components/EmptyListState"; import { EmptyListState } from "../../components/EmptyListState";
@ -38,7 +38,7 @@ export default function Filters() {
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<button <button
type="button" type="button"
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
onClick={toggleCreateFilter} onClick={toggleCreateFilter}
> >
Add new Add new
@ -48,8 +48,8 @@ export default function Filters() {
</header> </header>
<div className="max-w-7xl mx-auto pb-12 px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto pb-12 px-4 sm:px-6 lg:px-8">
<div className="bg-white rounded-lg shadow"> <div className="bg-white dark:bg-gray-900 light:rounded-lg light:shadow">
<div className="relative inset-0 py-3 px-3 sm:px-3 lg:px-3 h-full"> <div className="relative inset-0 light:py-3 light:px-3 light:sm:px-3 light:lg:px-3 h-full">
{data && data.length > 0 ? <FilterList filters={data} /> : {data && data.length > 0 ? <FilterList filters={data} /> :
<EmptyListState text="No filters here.." buttonText="Add new" buttonOnClick={toggleCreateFilter} />} <EmptyListState text="No filters here.." buttonText="Add new" buttonOnClick={toggleCreateFilter} />}
</div> </div>
@ -67,26 +67,26 @@ function FilterList({ filters }: FilterListProps) {
return ( return (
<div className="flex flex-col"> <div className="flex flex-col">
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"> <div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"> <div className="light:py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
<div className="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"> <div className="shadow overflow-hidden border-b border-gray-200 dark:border-gray-800 sm:rounded-lg">
<table className="min-w-full divide-y divide-gray-200"> <table className="min-w-full divide-y divide-gray-200 dark:divide-gray-800">
<thead className="bg-gray-50"> <thead className="bg-gray-50 dark:bg-gray-900 text-gray-500 dark:text-gray-400">
<tr> <tr>
<th <th
scope="col" scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider"
> >
Enabled Enabled
</th> </th>
<th <th
scope="col" scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider"
> >
Name Name
</th> </th>
<th <th
scope="col" scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider"
> >
Indexers Indexers
</th> </th>
@ -95,7 +95,7 @@ function FilterList({ filters }: FilterListProps) {
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody className="bg-white divide-y divide-gray-200"> <tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-800">
{filters.map((filter: Filter, idx) => ( {filters.map((filter: Filter, idx) => (
<FilterListItem filter={filter} key={idx} idx={idx} /> <FilterListItem filter={filter} key={idx} idx={idx} />
))} ))}
@ -124,14 +124,14 @@ function FilterListItem({ filter, idx }: FilterListItemProps) {
return ( return (
<tr key={filter.name} <tr key={filter.name}
className={idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}> className={idx % 2 === 0 ? 'bg-white dark:bg-gray-900' : 'bg-gray-50 dark:bg-gray-900'}>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-100">
<Switch <Switch
checked={enabled} checked={enabled}
onChange={toggleActive} onChange={toggleActive}
className={classNames( className={classNames(
enabled ? 'bg-teal-500' : 'bg-gray-200', enabled ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-700',
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500' 'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
)} )}
> >
<span className="sr-only">Use setting</span> <span className="sr-only">Use setting</span>
@ -139,16 +139,16 @@ function FilterListItem({ filter, idx }: FilterListItemProps) {
aria-hidden="true" aria-hidden="true"
className={classNames( className={classNames(
enabled ? 'translate-x-5' : 'translate-x-0', enabled ? 'translate-x-5' : 'translate-x-0',
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200' 'inline-block h-5 w-5 rounded-full bg-white dark:bg-gray-200 shadow transform ring-0 transition ease-in-out duration-200'
)} )}
/> />
</Switch> </Switch>
</td> </td>
<td className="px-6 py-4 w-full whitespace-nowrap text-sm font-medium text-gray-900">{filter.name}</td> <td className="px-6 py-4 w-full whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">{filter.name}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{filter.indexers && filter.indexers.map(t => <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{filter.indexers && filter.indexers.map(t =>
<span key={t.id} className="mr-2 inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-gray-100 text-gray-800">{t.name}</span>)}</td> <span key={t.id} className="mr-2 inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-400">{t.name}</span>)}</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<Link to={`filters/${filter.id.toString()}`} className="text-indigo-600 hover:text-indigo-900"> <Link to={`filters/${filter.id.toString()}`} className="text-indigo-600 dark:text-gray-200 hover:text-indigo-900 dark:hover:text-gray-400">
Edit Edit
</Link> </Link>
</td> </td>

View file

@ -22,11 +22,11 @@ function ApplicationSettings() {
) )
return ( return (
<form className="divide-y divide-gray-200 lg:col-span-9" action="#" method="POST"> <form className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9" action="#" method="POST">
<div className="py-6 px-4 sm:p-6 lg:pb-8"> <div className="py-6 px-4 sm:p-6 lg:pb-8">
<div> <div>
<h2 className="text-lg leading-6 font-medium text-gray-900">Application</h2> <h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Application</h2>
<p className="mt-1 text-sm text-gray-500"> <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Application settings. Change in config.toml and restart to take effect. Application settings. Change in config.toml and restart to take effect.
</p> </p>
</div> </div>
@ -35,7 +35,7 @@ function ApplicationSettings() {
<div className="mt-6 grid grid-cols-12 gap-6"> <div className="mt-6 grid grid-cols-12 gap-6">
<div className="col-span-6 sm:col-span-4"> <div className="col-span-6 sm:col-span-4">
<label htmlFor="host" className="block text-sm font-medium text-gray-700"> <label htmlFor="host" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
Host Host
</label> </label>
<input <input
@ -44,12 +44,12 @@ function ApplicationSettings() {
id="host" id="host"
value={data.host} value={data.host}
disabled={true} disabled={true}
className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-light-blue-500 focus:border-light-blue-500 sm:text-sm" className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
/> />
</div> </div>
<div className="col-span-6 sm:col-span-4"> <div className="col-span-6 sm:col-span-4">
<label htmlFor="port" className="block text-sm font-medium text-gray-700"> <label htmlFor="port" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
Port Port
</label> </label>
<input <input
@ -58,12 +58,12 @@ function ApplicationSettings() {
id="port" id="port"
value={data.port} value={data.port}
disabled={true} disabled={true}
className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-light-blue-500 focus:border-light-blue-500 sm:text-sm" className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
/> />
</div> </div>
<div className="col-span-6 sm:col-span-4"> <div className="col-span-6 sm:col-span-4">
<label htmlFor="base_url" className="block text-sm font-medium text-gray-700"> <label htmlFor="base_url" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
Base url Base url
</label> </label>
<input <input
@ -72,23 +72,23 @@ function ApplicationSettings() {
id="base_url" id="base_url"
value={data.base_url} value={data.base_url}
disabled={true} disabled={true}
className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-light-blue-500 focus:border-light-blue-500 sm:text-sm" className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
/> />
</div> </div>
</div> </div>
)} )}
</div> </div>
<div className="pt-6 pb-6 divide-y divide-gray-200"> <div className="pt-6 pb-6 divide-y divide-gray-200 dark:divide-gray-700">
<div className="px-4 sm:px-6"> <div className="px-4 sm:px-6">
<ul className="mt-2 divide-y divide-gray-200"> <ul className="mt-2 divide-y divide-gray-200">
<Switch.Group as="li" className="py-4 flex items-center justify-between"> <Switch.Group as="li" className="py-4 flex items-center justify-between">
<div className="flex flex-col"> <div className="flex flex-col">
<Switch.Label as="p" className="text-sm font-medium text-gray-900" <Switch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-white"
passive> passive>
Debug Debug
</Switch.Label> </Switch.Label>
<Switch.Description className="text-sm text-gray-500"> <Switch.Description className="text-sm text-gray-500 dark:text-gray-400">
Enable debug mode to get more logs. Enable debug mode to get more logs.
</Switch.Description> </Switch.Description>
</div> </div>
@ -96,8 +96,8 @@ function ApplicationSettings() {
checked={isDebug} checked={isDebug}
onChange={setIsDebug} onChange={setIsDebug}
className={classNames( className={classNames(
isDebug ? 'bg-teal-500' : 'bg-gray-200', isDebug ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-700',
'ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500' 'ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
)} )}
> >
<span className="sr-only">Use setting</span> <span className="sr-only">Use setting</span>

View file

@ -1,13 +1,12 @@
import {useState} from "react"; import { DownloadClient } from "../../domain/interfaces";
import {DownloadClient} from "../../domain/interfaces"; import { useToggle } from "../../hooks/hooks";
import {useToggle} from "../../hooks/hooks"; import { Switch } from "@headlessui/react";
import {Switch} from "@headlessui/react"; import { useQuery } from "react-query";
import {useQuery} from "react-query"; import { classNames } from "../../styles/utils";
import {classNames} from "../../styles/utils";
import { DownloadClientAddForm, DownloadClientUpdateForm } from "../../forms"; import { DownloadClientAddForm, DownloadClientUpdateForm } from "../../forms";
import EmptySimple from "../../components/empty/EmptySimple"; import EmptySimple from "../../components/empty/EmptySimple";
import APIClient from "../../api/APIClient"; import APIClient from "../../api/APIClient";
import {DownloadClientTypeNameMap} from "../../domain/constants"; import { DownloadClientTypeNameMap } from "../../domain/constants";
interface DownloadLClientSettingsListItemProps { interface DownloadLClientSettingsListItemProps {
client: DownloadClient; client: DownloadClient;
@ -18,17 +17,17 @@ function DownloadClientSettingsListItem({ client, idx }: DownloadLClientSettings
const [updateClientIsOpen, toggleUpdateClient] = useToggle(false) const [updateClientIsOpen, toggleUpdateClient] = useToggle(false)
return ( return (
<tr key={client.name} className={idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}> <tr key={client.name} className={idx % 2 === 0 ? 'light:bg-white' : 'light:bg-gray-50'}>
{updateClientIsOpen && {updateClientIsOpen &&
<DownloadClientUpdateForm client={client} isOpen={updateClientIsOpen} toggle={toggleUpdateClient}/> <DownloadClientUpdateForm client={client} isOpen={updateClientIsOpen} toggle={toggleUpdateClient} />
} }
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<Switch <Switch
checked={client.enabled} checked={client.enabled}
onChange={toggleUpdateClient} onChange={toggleUpdateClient}
className={classNames( className={classNames(
client.enabled ? 'bg-teal-500' : 'bg-gray-200', client.enabled ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600',
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500' 'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
)} )}
> >
<span className="sr-only">Use setting</span> <span className="sr-only">Use setting</span>
@ -41,11 +40,11 @@ function DownloadClientSettingsListItem({ client, idx }: DownloadLClientSettings
/> />
</Switch> </Switch>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{client.name}</td> <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{client.name}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{client.host}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">{client.host}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{DownloadClientTypeNameMap[client.type]}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">{DownloadClientTypeNameMap[client.type]}</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<span className="text-indigo-600 hover:text-indigo-900 cursor-pointer" onClick={toggleUpdateClient}> <span className="text-indigo-600 dark:text-gray-300 hover:text-indigo-900 cursor-pointer" onClick={toggleUpdateClient}>
Edit Edit
</span> </span>
</td> </td>
@ -67,21 +66,21 @@ function DownloadClientSettings() {
<div className="divide-y divide-gray-200 lg:col-span-9"> <div className="divide-y divide-gray-200 lg:col-span-9">
{addClientIsOpen && {addClientIsOpen &&
<DownloadClientAddForm isOpen={addClientIsOpen} toggle={toggleAddClient}/> <DownloadClientAddForm isOpen={addClientIsOpen} toggle={toggleAddClient} />
} }
<div className="py-6 px-4 sm:p-6 lg:pb-8"> <div className="py-6 px-4 sm:p-6 lg:pb-8">
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap"> <div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
<div className="ml-4 mt-4"> <div className="ml-4 mt-4">
<h3 className="text-lg leading-6 font-medium text-gray-900">Clients</h3> <h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Clients</h3>
<p className="mt-1 text-sm text-gray-500"> <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Manage download clients. Manage download clients.
</p> </p>
</div> </div>
<div className="ml-4 mt-4 flex-shrink-0"> <div className="ml-4 mt-4 flex-shrink-0">
<button <button
type="button" type="button"
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
onClick={toggleAddClient} onClick={toggleAddClient}
> >
Add new Add new
@ -93,43 +92,43 @@ function DownloadClientSettings() {
{data && data.length > 0 ? {data && data.length > 0 ?
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"> <div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"> <div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
<div className="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"> <div className="light:shadow overflow-hidden light:border-b light:border-gray-200 sm:rounded-lg">
<table className="min-w-full divide-y divide-gray-200"> <table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50"> <thead className="light:bg-gray-50">
<tr> <tr>
<th <th
scope="col" scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
> >
Enabled Enabled
</th> </th>
<th <th
scope="col" scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
> >
Name Name
</th> </th>
<th <th
scope="col" scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
> >
Host Host
</th> </th>
<th <th
scope="col" scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
> >
Type Type
</th> </th>
<th scope="col" className="relative px-6 py-3"> <th scope="col" className="relative px-6 py-3">
<span className="sr-only">Edit</span> <span className="sr-only">Edit</span>
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody className="light:bg-white divide-y divide-gray-200 dark:divide-gray-700">
{data && data.map((client, idx) => ( {data && data.map((client, idx) => (
<DownloadClientSettingsListItem client={client} idx={idx} key={idx} /> <DownloadClientSettingsListItem client={client} idx={idx} key={idx} />
))} ))}
</tbody> </tbody>
</table> </table>
</div> </div>

View file

@ -1,52 +1,53 @@
import {useToggle} from "../../hooks/hooks"; import { useEffect } from "react";
import {useQuery} from "react-query"; import { useToggle } from "../../hooks/hooks";
import React, {useEffect} from "react"; import { useQuery } from "react-query";
import {IndexerAddForm, IndexerUpdateForm} from "../../forms"; import { IndexerAddForm, IndexerUpdateForm } from "../../forms";
import {Indexer} from "../../domain/interfaces"; import { Indexer } from "../../domain/interfaces";
import {Switch} from "@headlessui/react"; import { Switch } from "@headlessui/react";
import {classNames} from "../../styles/utils"; import { classNames } from "../../styles/utils";
import EmptySimple from "../../components/empty/EmptySimple"; import EmptySimple from "../../components/empty/EmptySimple";
import APIClient from "../../api/APIClient"; import APIClient from "../../api/APIClient";
const ListItem = ({ indexer }: any) => { const ListItem = ({ indexer }: any) => {
const [updateIsOpen, toggleUpdate] = useToggle(false) const [updateIsOpen, toggleUpdate] = useToggle(false)
return ( return (
<tr key={indexer.name}> <tr key={indexer.name}>
{updateIsOpen && <IndexerUpdateForm isOpen={updateIsOpen} toggle={toggleUpdate} indexer={indexer} />} <IndexerUpdateForm isOpen={updateIsOpen} toggle={toggleUpdate} indexer={indexer} />
<td className="px-6 py-4 whitespace-nowrap">
<Switch <td className="px-6 py-4 whitespace-nowrap">
checked={indexer.enabled} <Switch
onChange={toggleUpdate} checked={indexer.enabled}
className={classNames( onChange={toggleUpdate}
indexer.enabled ? 'bg-teal-500' : 'bg-gray-200',
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500'
)}
>
<span className="sr-only">Enable</span>
<span
aria-hidden="true"
className={classNames( className={classNames(
indexer.enabled ? 'translate-x-5' : 'translate-x-0', indexer.enabled ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600',
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200' 'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
)} )}
/> >
</Switch> <span className="sr-only">Enable</span>
</td> <span
<td className="px-6 py-4 w-full whitespace-nowrap text-sm font-medium text-gray-900">{indexer.name}</td> aria-hidden="true"
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> className={classNames(
<span className="text-indigo-600 hover:text-indigo-900 cursor-pointer" onClick={toggleUpdate}> indexer.enabled ? 'translate-x-5' : 'translate-x-0',
Edit 'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
</span> )}
</td> />
</tr> </Switch>
) </td>
<td className="px-6 py-4 w-full whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{indexer.name}</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<span className="text-indigo-600 dark:text-gray-300 hover:text-indigo-900 dark:hover:text-blue-500 cursor-pointer" onClick={toggleUpdate}>
Edit
</span>
</td>
</tr>
)
} }
function IndexerSettings() { function IndexerSettings() {
const [addIndexerIsOpen, toggleAddIndexer] = useToggle(false) const [addIndexerIsOpen, toggleAddIndexer] = useToggle(false)
const {error, data} = useQuery<any[], Error>('indexer', APIClient.indexers.getAll, const { error, data } = useQuery<any[], Error>('indexer', APIClient.indexers.getAll,
{ {
refetchOnWindowFocus: false refetchOnWindowFocus: false
} }
@ -65,8 +66,8 @@ function IndexerSettings() {
<div className="py-6 px-4 sm:p-6 lg:pb-8"> <div className="py-6 px-4 sm:p-6 lg:pb-8">
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap"> <div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
<div className="ml-4 mt-4"> <div className="ml-4 mt-4">
<h3 className="text-lg leading-6 font-medium text-gray-900">Indexers</h3> <h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Indexers</h3>
<p className="mt-1 text-sm text-gray-500"> <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Indexer settings. Indexer settings.
</p> </p>
</div> </div>
@ -74,7 +75,7 @@ function IndexerSettings() {
<button <button
type="button" type="button"
onClick={toggleAddIndexer} onClick={toggleAddIndexer}
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
> >
Add new Add new
</button> </button>
@ -85,37 +86,37 @@ function IndexerSettings() {
{data && data.length > 0 ? {data && data.length > 0 ?
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"> <div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"> <div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
<div className="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"> <div className="light:shadow overflow-hidden light:border-b light:border-gray-200 dark:border-gray-700 sm:rounded-lg">
<table className="min-w-full divide-y divide-gray-200"> <table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50"> <thead className="light:bg-gray-50">
<tr> <tr>
<th <th
scope="col" scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
> >
Enabled Enabled
</th> </th>
<th <th
scope="col" scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
> >
Name Name
</th> </th>
<th scope="col" className="relative px-6 py-3"> <th scope="col" className="relative px-6 py-3">
<span className="sr-only">Edit</span> <span className="sr-only">Edit</span>
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody className="bg-white divide-y divide-gray-200"> <tbody className="light:bg-white divide-y divide-gray-200 dark:divide-gray-700">
{data && data.map((indexer: Indexer, idx: number) => ( {data && data.map((indexer: Indexer, idx: number) => (
<ListItem indexer={indexer} key={idx}/> <ListItem indexer={indexer} key={idx} />
))} ))}
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
</div> </div>
: <EmptySimple title="No indexers" subtitle="Add a new indexer" buttonText="New indexer" buttonAction={toggleAddIndexer}/> : <EmptySimple title="No indexers" subtitle="Add a new indexer" buttonText="New indexer" buttonAction={toggleAddIndexer} />
} }
</div> </div>

View file

@ -1,10 +1,9 @@
import React, {useEffect} from "react"; import { useEffect } from "react";
import {IrcNetworkAddForm} from "../../forms"; import { IrcNetworkAddForm, IrcNetworkUpdateForm } from "../../forms";
import {useToggle} from "../../hooks/hooks"; import { useToggle } from "../../hooks/hooks";
import {useQuery} from "react-query"; import { useQuery } from "react-query";
import IrcNetworkUpdateForm from "../../forms/settings/IrcNetworkUpdateForm"; import { Switch } from "@headlessui/react";
import {Switch} from "@headlessui/react"; import { classNames } from "../../styles/utils";
import {classNames} from "../../styles/utils";
import EmptySimple from "../../components/empty/EmptySimple"; import EmptySimple from "../../components/empty/EmptySimple";
import APIClient from "../../api/APIClient"; import APIClient from "../../api/APIClient";
@ -34,16 +33,13 @@ function IrcSettings() {
return ( return (
<div className="divide-y divide-gray-200 lg:col-span-9"> <div className="divide-y divide-gray-200 lg:col-span-9">
<IrcNetworkAddForm isOpen={addNetworkIsOpen} toggle={toggleAddNetwork} />
{addNetworkIsOpen &&
<IrcNetworkAddForm isOpen={addNetworkIsOpen} toggle={toggleAddNetwork}/>
}
<div className="py-6 px-4 sm:p-6 lg:pb-8"> <div className="py-6 px-4 sm:p-6 lg:pb-8">
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap"> <div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
<div className="ml-4 mt-4"> <div className="ml-4 mt-4">
<h3 className="text-lg leading-6 font-medium text-gray-900">IRC</h3> <h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">IRC</h3>
<p className="mt-1 text-sm text-gray-500"> <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
IRC networks and channels. IRC networks and channels.
</p> </p>
</div> </div>
@ -51,7 +47,7 @@ function IrcSettings() {
<button <button
type="button" type="button"
onClick={toggleAddNetwork} onClick={toggleAddNetwork}
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
> >
Add new Add new
</button> </button>
@ -62,49 +58,49 @@ function IrcSettings() {
{data && data.length > 0 ? {data && data.length > 0 ?
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"> <div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"> <div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
<div className="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"> <div className="light:shadow overflow-hidden light:border-b light:border-gray-200 sm:rounded-lg">
<table className="min-w-full divide-y divide-gray-200"> <table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50"> <thead className="light:bg-gray-50">
<tr> <tr>
<th <th
scope="col" scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
> >
Enabled Enabled
</th> </th>
<th <th
scope="col" scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
> >
Network Network
</th> </th>
<th <th
scope="col" scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
> >
Server Server
</th> </th>
<th <th
scope="col" scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
> >
Nick Nick
</th> </th>
<th scope="col" className="relative px-6 py-3"> <th scope="col" className="relative px-6 py-3">
<span className="sr-only">Edit</span> <span className="sr-only">Edit</span>
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody className="light:bg-white divide-y divide-gray-200 dark:divide-gray-700">
{data && data.map((network: IrcNetwork, idx) => ( {data && data.map((network: IrcNetwork, idx) => (
<ListItem key={idx} idx={idx} network={network}/> <ListItem key={idx} idx={idx} network={network} />
))} ))}
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
</div> </div>
: <EmptySimple title="No networks" subtitle="Add a new network" buttonText="New network" buttonAction={toggleAddNetwork}/> : <EmptySimple title="No networks" subtitle="Add a new network" buttonText="New network" buttonAction={toggleAddNetwork} />
} }
</div> </div>
</div> </div>
@ -116,15 +112,16 @@ const ListItem = ({ idx, network }: any) => {
const [updateIsOpen, toggleUpdate] = useToggle(false) const [updateIsOpen, toggleUpdate] = useToggle(false)
return ( return (
<tr key={network.name} className={idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}> <tr key={network.name} className={idx % 2 === 0 ? 'light:bg-white' : 'light:bg-gray-50'}>
{updateIsOpen && <IrcNetworkUpdateForm isOpen={updateIsOpen} toggle={toggleUpdate} network={network} />} <IrcNetworkUpdateForm isOpen={updateIsOpen} toggle={toggleUpdate} network={network} />
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
<Switch <Switch
checked={network.enabled} checked={network.enabled}
onChange={toggleUpdate} onChange={toggleUpdate}
className={classNames( className={classNames(
network.enabled ? 'bg-teal-500' : 'bg-gray-200', network.enabled ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600',
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500' 'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
)} )}
> >
<span className="sr-only">Enable</span> <span className="sr-only">Enable</span>
@ -137,11 +134,11 @@ const ListItem = ({ idx, network }: any) => {
/> />
</Switch> </Switch>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{network.name}</td> <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{network.name}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{network.server}:{network.port} {network.tls && <span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">TLS</span>}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"><span>{network.server}:{network.port}</span> {network.tls && <span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 dark:bg-green-300 text-green-800 dark:text-green-900">TLS</span>}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{network.nickserv?.account}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">{network.nickserv?.account}</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<span className="text-indigo-600 hover:text-indigo-900 cursor-pointer" onClick={toggleUpdate}> <span className="text-indigo-600 dark:text-gray-300 hover:text-indigo-900 cursor-pointer" onClick={toggleUpdate}>
Edit Edit
</span> </span>
</td> </td>

View file

@ -21,7 +21,7 @@ module.exports = {
'col-span-12', 'col-span-12',
], ],
}, },
darkMode: false, // or 'media' or 'class' darkMode: 'media', // or 'media' or 'class'
theme: { theme: {
extend: { extend: {
colors: { colors: {