diff --git a/web/package.json b/web/package.json index 20075ff..122e207 100644 --- a/web/package.json +++ b/web/package.json @@ -22,6 +22,7 @@ "react-dom": "^17.0.2", "react-final-form": "^6.5.3", "react-final-form-arrays": "^3.1.3", + "react-hot-toast": "^2.1.1", "react-multi-select-component": "^4.0.2", "react-query": "^3.18.1", "react-router-dom": "^5.2.0", diff --git a/web/src/components/Layout.tsx b/web/src/components/Layout.tsx index 365d6e4..c018d51 100644 --- a/web/src/components/Layout.tsx +++ b/web/src/components/Layout.tsx @@ -4,6 +4,7 @@ import {useEffect, useState} from "react"; import { Fragment } from "react"; import {Redirect} from "react-router-dom"; import APIClient from "../api/APIClient"; +import { Toaster } from "react-hot-toast"; export default function Layout({auth=false, authFallback="/login", children}: any) { const [loggedIn, setLoggedIn] = useRecoilState(isLoggedIn); @@ -28,6 +29,7 @@ export default function Layout({auth=false, authFallback="/login", children}: an {auth && !loggedIn ? : ( + {children} )} diff --git a/web/src/components/notifications/Toast.tsx b/web/src/components/notifications/Toast.tsx new file mode 100644 index 0000000..18c5a46 --- /dev/null +++ b/web/src/components/notifications/Toast.tsx @@ -0,0 +1,52 @@ +import { FC } from 'react' +import { XIcon, CheckCircleIcon, ExclamationIcon, ExclamationCircleIcon } from '@heroicons/react/solid' +import { toast } from 'react-hot-toast' + +type Props = { + type: 'error' | 'success' | 'warning' + body?: string + t?: any; +} + +const Toast: FC = ({ + type, + body, + t +}) => { + return ( +
+
+
+
+ {type === 'success' &&
+
+

+ {type === 'success' && "Success"} + {type === 'error' && "Error"} + {type === 'warning' && "Warning"} +

+

{body}

+
+
+ +
+
+
+
+ ) +} + +export default Toast; \ No newline at end of file diff --git a/web/src/forms/filters/FilterActionAddForm.tsx b/web/src/forms/filters/FilterActionAddForm.tsx index b9d4cbd..50b3e79 100644 --- a/web/src/forms/filters/FilterActionAddForm.tsx +++ b/web/src/forms/filters/FilterActionAddForm.tsx @@ -17,6 +17,9 @@ import { RadioFieldsetWide, } from "../../components/inputs/wide"; +import { toast } from 'react-hot-toast' +import Toast from '../../components/notifications/Toast'; + interface DownloadClientSelectProps { name: string; clients: DownloadClient[]; @@ -135,6 +138,8 @@ function FilterActionAddForm({ filter, isOpen, toggle, clients }: props) { { onSuccess: () => { queryClient.invalidateQueries(["filter", filter.id]); + toast.custom((t) => ) + sleep(500).then(() => toggle()); }, } diff --git a/web/src/forms/filters/FilterActionUpdateForm.tsx b/web/src/forms/filters/FilterActionUpdateForm.tsx index 9ca7855..891d417 100644 --- a/web/src/forms/filters/FilterActionUpdateForm.tsx +++ b/web/src/forms/filters/FilterActionUpdateForm.tsx @@ -17,6 +17,10 @@ import { } from "../../components/inputs/wide"; import { DownloadClientSelect } from "./FilterActionAddForm"; +import { toast } from 'react-hot-toast' +import Toast from '../../components/notifications/Toast'; + + interface props { filter: Filter; isOpen: boolean; @@ -38,6 +42,8 @@ function FilterActionUpdateForm({ onSuccess: () => { // console.log("add action"); queryClient.invalidateQueries(["filter", filter.id]); + toast.custom((t) => ) + sleep(1500); toggle(); diff --git a/web/src/forms/filters/FilterAddForm.tsx b/web/src/forms/filters/FilterAddForm.tsx index 0dd0c83..8c08a02 100644 --- a/web/src/forms/filters/FilterAddForm.tsx +++ b/web/src/forms/filters/FilterAddForm.tsx @@ -8,12 +8,18 @@ import {Field, Form} from "react-final-form"; import DEBUG from "../../components/debug"; import APIClient from "../../api/APIClient"; +import { toast } from 'react-hot-toast' +import Toast from '../../components/notifications/Toast'; + + const required = (value: any) => (value ? undefined : 'Required') function FilterAddForm({isOpen, toggle}: any) { const mutation = useMutation((filter: Filter) => APIClient.filters.create(filter), { onSuccess: () => { queryClient.invalidateQueries('filter'); + toast.custom((t) => ) + toggle() } }) diff --git a/web/src/forms/settings/IndexerAddForm.tsx b/web/src/forms/settings/IndexerAddForm.tsx index 4f24567..9469b8b 100644 --- a/web/src/forms/settings/IndexerAddForm.tsx +++ b/web/src/forms/settings/IndexerAddForm.tsx @@ -11,7 +11,8 @@ import { queryClient } from "../../App"; import { SwitchGroup, TextFieldWide } from "../../components/inputs"; import APIClient from "../../api/APIClient"; import { NumberFieldWide, PasswordFieldWide } from "../../components/inputs/wide"; - +import { toast } from 'react-hot-toast' +import Toast from '../../components/notifications/Toast'; interface props { isOpen: boolean; toggle: any; @@ -28,9 +29,12 @@ function IndexerAddForm({ isOpen, toggle }: props) { const mutation = useMutation((indexer: Indexer) => APIClient.indexers.create(indexer), { onSuccess: () => { queryClient.invalidateQueries(['indexer']); + toast.custom((t) => ) sleep(1500) - toggle() + }, + onError: () => { + toast.custom((t) => ) } }) diff --git a/web/src/forms/settings/IndexerUpdateForm.tsx b/web/src/forms/settings/IndexerUpdateForm.tsx index f962336..db03eb2 100644 --- a/web/src/forms/settings/IndexerUpdateForm.tsx +++ b/web/src/forms/settings/IndexerUpdateForm.tsx @@ -12,6 +12,9 @@ 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; @@ -24,6 +27,7 @@ function IndexerUpdateForm({ isOpen, toggle, indexer }: props) { const mutation = useMutation((indexer: Indexer) => APIClient.indexers.update(indexer), { onSuccess: () => { queryClient.invalidateQueries(['indexer']); + toast.custom((t) => ) sleep(1500) toggle() @@ -33,6 +37,7 @@ function IndexerUpdateForm({ isOpen, toggle, indexer }: props) { const deleteMutation = useMutation((id: number) => APIClient.indexers.delete(id), { onSuccess: () => { queryClient.invalidateQueries(['indexer']); + toast.custom((t) => ) } }) diff --git a/web/src/forms/settings/IrcNetworkAddForm.tsx b/web/src/forms/settings/IrcNetworkAddForm.tsx index 111aea9..fe2536c 100644 --- a/web/src/forms/settings/IrcNetworkAddForm.tsx +++ b/web/src/forms/settings/IrcNetworkAddForm.tsx @@ -14,17 +14,31 @@ 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 => { + onSuccess: (data) => { queryClient.invalidateQueries(['networks']); + toast.custom((t) => ) toggle() - } + }, + onError: () => { + toast.custom((t) => ) + }, }) 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. let cmds = data.connect_commands && data.connect_commands.length > 0 ? data.connect_commands.replace(/\r\n/g,"\n").split("\n") : []; @@ -35,22 +49,34 @@ function IrcNetworkAddForm({isOpen, toggle}: any) { }; const validate = (values: any) => { - const errors = {} as 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 ( - +
@@ -73,6 +99,9 @@ function IrcNetworkAddForm({isOpen, toggle}: any) { server: "", tls: false, pass: "", + nickserv: { + account: "" + } }} mutators={{ ...arrayMutators @@ -119,7 +148,7 @@ function IrcNetworkAddForm({isOpen, toggle}: any) {
- +
@@ -127,7 +156,7 @@ function IrcNetworkAddForm({isOpen, toggle}: any) { - + diff --git a/web/src/forms/settings/IrcNetworkUpdateForm.tsx b/web/src/forms/settings/IrcNetworkUpdateForm.tsx index 3de05f1..aef7303 100644 --- a/web/src/forms/settings/IrcNetworkUpdateForm.tsx +++ b/web/src/forms/settings/IrcNetworkUpdateForm.tsx @@ -16,12 +16,16 @@ 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) => ) toggle() } }) @@ -29,6 +33,8 @@ function IrcNetworkUpdateForm({ isOpen, toggle, network }: any) { const deleteMutation = useMutation((id: number) => APIClient.irc.deleteNetwork(id), { onSuccess: () => { queryClient.invalidateQueries(['networks']); + toast.custom((t) => ) + toggle() } }) @@ -52,7 +58,11 @@ function IrcNetworkUpdateForm({ isOpen, toggle, network }: any) { }; const validate = (values: any) => { - const errors = {} as any; + const errors = { + nickserv: { + account: null, + } + } as any; if (!values.name) { errors.name = "Required"; @@ -66,7 +76,7 @@ function IrcNetworkUpdateForm({ isOpen, toggle, network }: any) { errors.port = "Required"; } - if (!values.nickserv.account) { + if(!values.nickserv?.account) { errors.nickserv.account = "Required"; } diff --git a/web/src/forms/settings/downloadclient/DownloadClientAddForm.tsx b/web/src/forms/settings/downloadclient/DownloadClientAddForm.tsx index 84e2dcf..a42c86a 100644 --- a/web/src/forms/settings/downloadclient/DownloadClientAddForm.tsx +++ b/web/src/forms/settings/downloadclient/DownloadClientAddForm.tsx @@ -17,6 +17,9 @@ import { DownloadClientTypeOptions } from "../../../domain/constants"; import { RadioFieldsetWide } from "../../../components/inputs/wide"; import { componentMap } 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); @@ -27,8 +30,13 @@ function DownloadClientAddForm({ isOpen, toggle }: any) { { onSuccess: () => { queryClient.invalidateQueries(["downloadClients"]); + toast.custom((t) => ) + toggle(); }, + onError: () => { + toast.custom((t) => ) + } } ); @@ -53,6 +61,7 @@ function DownloadClientAddForm({ isOpen, toggle }: any) { }); }, onError: (error) => { + console.log('not added') setIsTesting(false); setIsErrorTest(true); sleep(2500).then(() => { diff --git a/web/src/forms/settings/downloadclient/DownloadClientUpdateForm.tsx b/web/src/forms/settings/downloadclient/DownloadClientUpdateForm.tsx index 850304a..67b11e4 100644 --- a/web/src/forms/settings/downloadclient/DownloadClientUpdateForm.tsx +++ b/web/src/forms/settings/downloadclient/DownloadClientUpdateForm.tsx @@ -16,6 +16,10 @@ import { componentMap } 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); @@ -27,7 +31,7 @@ function DownloadClientUpdateForm({ client, isOpen, toggle }: any) { { onSuccess: () => { queryClient.invalidateQueries(["downloadClients"]); - + toast.custom((t) => ) toggle(); }, } @@ -38,6 +42,7 @@ function DownloadClientUpdateForm({ client, isOpen, toggle }: any) { { onSuccess: () => { queryClient.invalidateQueries(); + toast.custom((t) => ) toggleDeleteModal(); }, } diff --git a/web/src/index.css b/web/src/index.css index 17df0e7..2b3ea29 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -15,3 +15,35 @@ code { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } + +@keyframes enter { + 0% { + transform: scale(.9); + opacity: 0 + } + + to { + transform: scale(1); + opacity: 1 + } +} + +.animate-enter { + animation: enter .2s ease-out +} + +@keyframes leave { + 0% { + transform: scale(1); + opacity: 1 + } + + to { + transform: scale(.9); + opacity: 0 + } +} + +.animate-leave { + animation: leave .15s ease-in forwards +} \ No newline at end of file diff --git a/web/src/screens/Filters.tsx b/web/src/screens/Filters.tsx index 9e33df9..6e55f46 100644 --- a/web/src/screens/Filters.tsx +++ b/web/src/screens/Filters.tsx @@ -29,6 +29,10 @@ import { FilterAddForm, FilterActionAddForm} from "../forms"; import Select from "react-select"; import APIClient from "../api/APIClient"; +import { toast } from 'react-hot-toast' +import Toast from '../components/notifications/Toast'; + + const tabs = [ {name: 'General', href: '', current: true}, // { name: 'TV', href: 'tv', current: false }, @@ -443,6 +447,8 @@ function FilterTabGeneral({filter}: FilterTabGeneralProps) { const updateMutation = useMutation((filter: Filter) => APIClient.filters.update(filter), { onSuccess: () => { // queryClient.setQueryData(['filter', filter.id], data) + toast.custom((t) => ) + queryClient.invalidateQueries(["filter",filter.id]); } }) @@ -451,6 +457,8 @@ function FilterTabGeneral({filter}: FilterTabGeneralProps) { onSuccess: () => { // invalidate filters queryClient.invalidateQueries("filter"); + toast.custom((t) => ) + // redirect history.push("/filters") } @@ -568,6 +576,7 @@ function FilterTabMoviesTvNew2({filter}: FilterTabGeneralProps) { const updateMutation = useMutation((filter: Filter) => APIClient.filters.update(filter), { onSuccess: () => { // queryClient.setQueryData(['filter', filter.id], data) + toast.custom((t) => ) queryClient.invalidateQueries(["filter",filter.id]); } }) @@ -575,6 +584,7 @@ function FilterTabMoviesTvNew2({filter}: FilterTabGeneralProps) { const deleteMutation = useMutation((id: number) => APIClient.filters.delete(id), { onSuccess: () => { // invalidate filters + toast.custom((t) => ) queryClient.invalidateQueries("filter"); // redirect history.push("/filters") @@ -677,6 +687,8 @@ function FilterTabAdvanced({filter}: FilterTabGeneralProps) { const updateMutation = useMutation((filter: Filter) => APIClient.filters.update(filter), { onSuccess: () => { // queryClient.setQueryData(['filter', filter.id], data) + toast.custom((t) => ) + queryClient.invalidateQueries(["filter",filter.id]); } }) @@ -685,6 +697,8 @@ function FilterTabAdvanced({filter}: FilterTabGeneralProps) { onSuccess: () => { // invalidate filters queryClient.invalidateQueries("filter"); + toast.custom((t) => ) + // redirect history.push("/filters") } diff --git a/web/tailwind.config.js b/web/tailwind.config.js index b722fbd..ff08dbb 100644 --- a/web/tailwind.config.js +++ b/web/tailwind.config.js @@ -1,6 +1,7 @@ const colors = require('tailwindcss/colors') module.exports = { + mode: 'jit', purge: { content: [ './src/**/*.{tsx,ts,html,css}',