mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 00:39:13 +00:00
feat(web): vendor react-hot-toast (#1883)
* feat(web): vendor react-hot-toast * vendor react-hot-toast to be react 19 compatible * feat: remove react-hot-toast and add goober * chore: fix lint warnings
This commit is contained in:
parent
43c28fc0c6
commit
d8f578b5ea
46 changed files with 1071 additions and 59 deletions
|
@ -55,13 +55,13 @@
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"formik": "^2.4.6",
|
"formik": "^2.4.6",
|
||||||
|
"goober": "^2.1.16",
|
||||||
"http-proxy-middleware": "^3.0.3",
|
"http-proxy-middleware": "^3.0.3",
|
||||||
"postcss": "^8.4.49",
|
"postcss": "^8.4.49",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-debounce-input": "^3.3.0",
|
"react-debounce-input": "^3.3.0",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.53.2",
|
"react-hook-form": "^7.53.2",
|
||||||
"react-hot-toast": "^2.4.1",
|
|
||||||
"react-multi-select-component": "^4.3.4",
|
"react-multi-select-component": "^4.3.4",
|
||||||
"react-popper-tooltip": "^4.4.2",
|
"react-popper-tooltip": "^4.4.2",
|
||||||
"react-ridge-state": "4.2.9",
|
"react-ridge-state": "4.2.9",
|
||||||
|
|
30
web/pnpm-lock.yaml
generated
30
web/pnpm-lock.yaml
generated
|
@ -79,6 +79,9 @@ importers:
|
||||||
formik:
|
formik:
|
||||||
specifier: ^2.4.6
|
specifier: ^2.4.6
|
||||||
version: 2.4.6(react@18.3.1)
|
version: 2.4.6(react@18.3.1)
|
||||||
|
goober:
|
||||||
|
specifier: ^2.1.16
|
||||||
|
version: 2.1.16(csstype@3.1.2)
|
||||||
http-proxy-middleware:
|
http-proxy-middleware:
|
||||||
specifier: ^3.0.3
|
specifier: ^3.0.3
|
||||||
version: 3.0.3
|
version: 3.0.3
|
||||||
|
@ -97,9 +100,6 @@ importers:
|
||||||
react-hook-form:
|
react-hook-form:
|
||||||
specifier: ^7.53.2
|
specifier: ^7.53.2
|
||||||
version: 7.53.2(react@18.3.1)
|
version: 7.53.2(react@18.3.1)
|
||||||
react-hot-toast:
|
|
||||||
specifier: ^2.4.1
|
|
||||||
version: 2.4.1(csstype@3.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
|
||||||
react-multi-select-component:
|
react-multi-select-component:
|
||||||
specifier: ^4.3.4
|
specifier: ^4.3.4
|
||||||
version: 4.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 4.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
@ -2165,11 +2165,6 @@ packages:
|
||||||
resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==}
|
resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
goober@2.1.13:
|
|
||||||
resolution: {integrity: sha512-jFj3BQeleOoy7t93E9rZ2de+ScC4lQICLwiAQmKMg9F6roKGaLSHoCDYKkWlSafg138jejvq/mTdvmnwDQgqoQ==}
|
|
||||||
peerDependencies:
|
|
||||||
csstype: 3.1.2
|
|
||||||
|
|
||||||
goober@2.1.16:
|
goober@2.1.16:
|
||||||
resolution: {integrity: sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==}
|
resolution: {integrity: sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
@ -2826,13 +2821,6 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.3.1
|
react: ^18.3.1
|
||||||
|
|
||||||
react-hot-toast@2.4.1:
|
|
||||||
resolution: {integrity: sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
peerDependencies:
|
|
||||||
react: ^18.3.1
|
|
||||||
react-dom: '>=16'
|
|
||||||
|
|
||||||
react-is@16.13.1:
|
react-is@16.13.1:
|
||||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
|
|
||||||
|
@ -5812,10 +5800,6 @@ snapshots:
|
||||||
define-properties: 1.2.1
|
define-properties: 1.2.1
|
||||||
gopd: 1.0.1
|
gopd: 1.0.1
|
||||||
|
|
||||||
goober@2.1.13(csstype@3.1.2):
|
|
||||||
dependencies:
|
|
||||||
csstype: 3.1.2
|
|
||||||
|
|
||||||
goober@2.1.16(csstype@3.1.2):
|
goober@2.1.16(csstype@3.1.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
csstype: 3.1.2
|
csstype: 3.1.2
|
||||||
|
@ -6402,14 +6386,6 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
|
|
||||||
react-hot-toast@2.4.1(csstype@3.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
|
||||||
dependencies:
|
|
||||||
goober: 2.1.13(csstype@3.1.2)
|
|
||||||
react: 18.3.1
|
|
||||||
react-dom: 18.3.1(react@18.3.1)
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- csstype
|
|
||||||
|
|
||||||
react-is@16.13.1: {}
|
react-is@16.13.1: {}
|
||||||
|
|
||||||
react-multi-select-component@4.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
react-multi-select-component@4.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { RouterProvider } from "@tanstack/react-router"
|
import { RouterProvider } from "@tanstack/react-router"
|
||||||
import { QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { Toaster } from "react-hot-toast";
|
import { Toaster } from "@components/hot-toast";
|
||||||
import { Router } from "@app/routes";
|
import { Router } from "@app/routes";
|
||||||
import { routerBasePath } from "@utils";
|
import { routerBasePath } from "@utils";
|
||||||
import { queryClient } from "@api/QueryClient";
|
import { queryClient } from "@api/QueryClient";
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { QueryCache, QueryClient } from "@tanstack/react-query";
|
import { QueryCache, QueryClient } from "@tanstack/react-query";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "@components/hot-toast";
|
||||||
import Toast from "@components/notifications/Toast";
|
import Toast from "@components/notifications/Toast";
|
||||||
import { AuthContext } from "@utils/Context";
|
import { AuthContext } from "@utils/Context";
|
||||||
import { getRouteApi, redirect } from "@tanstack/react-router";
|
import { getRouteApi, redirect } from "@tanstack/react-router";
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
import { formatDistanceToNowStrict } from "date-fns";
|
import { formatDistanceToNowStrict } from "date-fns";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { CellContext } from "@tanstack/react-table";
|
import { CellContext } from "@tanstack/react-table";
|
||||||
|
@ -21,6 +20,7 @@ import { APIClient } from "@api/APIClient";
|
||||||
import { FilterKeys } from "@api/query_keys";
|
import { FilterKeys } from "@api/query_keys";
|
||||||
import { classNames, humanFileSize, simplifyDate } from "@utils";
|
import { classNames, humanFileSize, simplifyDate } from "@utils";
|
||||||
import { ExternalLink } from "../ExternalLink";
|
import { ExternalLink } from "../ExternalLink";
|
||||||
|
import { toast } from "@components/hot-toast";
|
||||||
import Toast from "@components/notifications/Toast";
|
import Toast from "@components/notifications/Toast";
|
||||||
import { RingResizeSpinner } from "@components/Icons";
|
import { RingResizeSpinner } from "@components/Icons";
|
||||||
import { Tooltip } from "@components/tooltips/Tooltip";
|
import { Tooltip } from "@components/tooltips/Tooltip";
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
import { useToggle } from "@hooks/hooks";
|
import { useToggle } from "@hooks/hooks";
|
||||||
import { CheckIcon, DocumentDuplicateIcon, EyeIcon, EyeSlashIcon } from "@heroicons/react/24/outline";
|
import { CheckIcon, DocumentDuplicateIcon, EyeIcon, EyeSlashIcon } from "@heroicons/react/24/outline";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "@components/hot-toast";
|
||||||
import Toast from "@components/notifications/Toast";
|
import Toast from "@components/notifications/Toast";
|
||||||
|
|
||||||
interface KeyFieldProps {
|
interface KeyFieldProps {
|
||||||
|
|
|
@ -3,13 +3,13 @@
|
||||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import toast from "react-hot-toast";
|
|
||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { getRouteApi, redirect } from "@tanstack/react-router";
|
import { getRouteApi, redirect } from "@tanstack/react-router";
|
||||||
import { Disclosure, DisclosureButton } from "@headlessui/react";
|
import { Disclosure, DisclosureButton } from "@headlessui/react";
|
||||||
import { Bars3Icon, XMarkIcon, MegaphoneIcon } from "@heroicons/react/24/outline";
|
import { Bars3Icon, XMarkIcon, MegaphoneIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
import { APIClient } from "@api/APIClient";
|
import { APIClient } from "@api/APIClient";
|
||||||
|
import toast from "@components/hot-toast";
|
||||||
import Toast from "@components/notifications/Toast";
|
import Toast from "@components/notifications/Toast";
|
||||||
|
|
||||||
import { LeftNav } from "./LeftNav";
|
import { LeftNav } from "./LeftNav";
|
||||||
|
|
61
web/src/components/hot-toast/components/checkmark.tsx
Normal file
61
web/src/components/hot-toast/components/checkmark.tsx
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import { styled, keyframes } from 'goober';
|
||||||
|
|
||||||
|
const circleAnimation = keyframes`
|
||||||
|
from {
|
||||||
|
transform: scale(0) rotate(45deg);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scale(1) rotate(45deg);
|
||||||
|
opacity: 1;
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const checkmarkAnimation = keyframes`
|
||||||
|
0% {
|
||||||
|
height: 0;
|
||||||
|
width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
height: 0;
|
||||||
|
width: 6px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
height: 10px;
|
||||||
|
}`;
|
||||||
|
|
||||||
|
export interface CheckmarkTheme {
|
||||||
|
primary?: string;
|
||||||
|
secondary?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CheckmarkIcon = styled('div')<CheckmarkTheme>`
|
||||||
|
width: 20px;
|
||||||
|
opacity: 0;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: ${(p) => p.primary || '#61d345'};
|
||||||
|
position: relative;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
|
||||||
|
animation: ${circleAnimation} 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275)
|
||||||
|
forwards;
|
||||||
|
animation-delay: 100ms;
|
||||||
|
&:after {
|
||||||
|
content: '';
|
||||||
|
box-sizing: border-box;
|
||||||
|
animation: ${checkmarkAnimation} 0.2s ease-out forwards;
|
||||||
|
opacity: 0;
|
||||||
|
animation-delay: 200ms;
|
||||||
|
position: absolute;
|
||||||
|
border-right: 2px solid;
|
||||||
|
border-bottom: 2px solid;
|
||||||
|
border-color: ${(p) => p.secondary || '#fff'};
|
||||||
|
bottom: 6px;
|
||||||
|
left: 6px;
|
||||||
|
height: 10px;
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
`;
|
71
web/src/components/hot-toast/components/error.tsx
Normal file
71
web/src/components/hot-toast/components/error.tsx
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import { styled, keyframes } from 'goober';
|
||||||
|
|
||||||
|
const circleAnimation = keyframes`
|
||||||
|
from {
|
||||||
|
transform: scale(0) rotate(45deg);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scale(1) rotate(45deg);
|
||||||
|
opacity: 1;
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const firstLineAnimation = keyframes`
|
||||||
|
from {
|
||||||
|
transform: scale(0);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const secondLineAnimation = keyframes`
|
||||||
|
from {
|
||||||
|
transform: scale(0) rotate(90deg);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scale(1) rotate(90deg);
|
||||||
|
opacity: 1;
|
||||||
|
}`;
|
||||||
|
|
||||||
|
export interface ErrorTheme {
|
||||||
|
primary?: string;
|
||||||
|
secondary?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ErrorIcon = styled('div')<ErrorTheme>`
|
||||||
|
width: 20px;
|
||||||
|
opacity: 0;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: ${(p) => p.primary || '#ff4b4b'};
|
||||||
|
position: relative;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
|
||||||
|
animation: ${circleAnimation} 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275)
|
||||||
|
forwards;
|
||||||
|
animation-delay: 100ms;
|
||||||
|
|
||||||
|
&:after,
|
||||||
|
&:before {
|
||||||
|
content: '';
|
||||||
|
animation: ${firstLineAnimation} 0.15s ease-out forwards;
|
||||||
|
animation-delay: 150ms;
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 3px;
|
||||||
|
opacity: 0;
|
||||||
|
background: ${(p) => p.secondary || '#fff'};
|
||||||
|
bottom: 9px;
|
||||||
|
left: 4px;
|
||||||
|
height: 2px;
|
||||||
|
width: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
animation: ${secondLineAnimation} 0.15s ease-out forwards;
|
||||||
|
animation-delay: 180ms;
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
`;
|
26
web/src/components/hot-toast/components/loader.tsx
Normal file
26
web/src/components/hot-toast/components/loader.tsx
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { styled, keyframes } from 'goober';
|
||||||
|
|
||||||
|
const rotate = keyframes`
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export interface LoaderTheme {
|
||||||
|
primary?: string;
|
||||||
|
secondary?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LoaderIcon = styled('div')<LoaderTheme>`
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 2px solid;
|
||||||
|
border-radius: 100%;
|
||||||
|
border-color: ${(p) => p.secondary || '#e0e0e0'};
|
||||||
|
border-right-color: ${(p) => p.primary || '#616161'};
|
||||||
|
animation: ${rotate} 1s linear infinite;
|
||||||
|
`;
|
111
web/src/components/hot-toast/components/toast-bar.tsx
Normal file
111
web/src/components/hot-toast/components/toast-bar.tsx
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { styled, keyframes } from 'goober';
|
||||||
|
|
||||||
|
import { Toast, ToastPosition, resolveValue, Renderable } from '../core/types';
|
||||||
|
import { ToastIcon } from './toast-icon';
|
||||||
|
import { prefersReducedMotion } from '../core/utils';
|
||||||
|
|
||||||
|
const enterAnimation = (factor: number) => `
|
||||||
|
0% {transform: translate3d(0,${factor * -200}%,0) scale(.6); opacity:.5;}
|
||||||
|
100% {transform: translate3d(0,0,0) scale(1); opacity:1;}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const exitAnimation = (factor: number) => `
|
||||||
|
0% {transform: translate3d(0,0,-1px) scale(1); opacity:1;}
|
||||||
|
100% {transform: translate3d(0,${factor * -150}%,-1px) scale(.6); opacity:0;}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const fadeInAnimation = `0%{opacity:0;} 100%{opacity:1;}`;
|
||||||
|
const fadeOutAnimation = `0%{opacity:1;} 100%{opacity:0;}`;
|
||||||
|
|
||||||
|
const ToastBarBase = styled('div')`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #fff;
|
||||||
|
color: #363636;
|
||||||
|
line-height: 1.3;
|
||||||
|
will-change: transform;
|
||||||
|
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1), 0 3px 3px rgba(0, 0, 0, 0.05);
|
||||||
|
max-width: 350px;
|
||||||
|
pointer-events: auto;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Message = styled('div')`
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 4px 10px;
|
||||||
|
color: inherit;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
white-space: pre-line;
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface ToastBarProps {
|
||||||
|
toast: Toast;
|
||||||
|
position?: ToastPosition;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
children?: (components: {
|
||||||
|
icon: Renderable;
|
||||||
|
message: Renderable;
|
||||||
|
}) => Renderable;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAnimationStyle = (
|
||||||
|
position: ToastPosition,
|
||||||
|
visible: boolean
|
||||||
|
): React.CSSProperties => {
|
||||||
|
const top = position.includes('top');
|
||||||
|
const factor = top ? 1 : -1;
|
||||||
|
|
||||||
|
const [enter, exit] = prefersReducedMotion()
|
||||||
|
? [fadeInAnimation, fadeOutAnimation]
|
||||||
|
: [enterAnimation(factor), exitAnimation(factor)];
|
||||||
|
|
||||||
|
return {
|
||||||
|
animation: visible
|
||||||
|
? `${keyframes(enter)} 0.35s cubic-bezier(.21,1.02,.73,1) forwards`
|
||||||
|
: `${keyframes(exit)} 0.4s forwards cubic-bezier(.06,.71,.55,1)`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ToastBar: React.FC<ToastBarProps> = React.memo(
|
||||||
|
({ toast, position, style, children }) => {
|
||||||
|
const animationStyle: React.CSSProperties = toast.height
|
||||||
|
? getAnimationStyle(
|
||||||
|
toast.position || position || 'top-center',
|
||||||
|
toast.visible
|
||||||
|
)
|
||||||
|
: { opacity: 0 };
|
||||||
|
|
||||||
|
const icon = <ToastIcon toast={toast} />;
|
||||||
|
const message = (
|
||||||
|
<Message {...toast.ariaProps}>
|
||||||
|
{resolveValue(toast.message, toast)}
|
||||||
|
</Message>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToastBarBase
|
||||||
|
className={toast.className}
|
||||||
|
style={{
|
||||||
|
...animationStyle,
|
||||||
|
...style,
|
||||||
|
...toast.style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{typeof children === 'function' ? (
|
||||||
|
children({
|
||||||
|
icon,
|
||||||
|
message,
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{icon}
|
||||||
|
{message}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ToastBarBase>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
77
web/src/components/hot-toast/components/toast-icon.tsx
Normal file
77
web/src/components/hot-toast/components/toast-icon.tsx
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { styled, keyframes } from 'goober';
|
||||||
|
|
||||||
|
import { Toast } from '../core/types';
|
||||||
|
import { ErrorIcon, ErrorTheme } from './error';
|
||||||
|
import { LoaderIcon, LoaderTheme } from './loader';
|
||||||
|
import { CheckmarkIcon, CheckmarkTheme } from './checkmark';
|
||||||
|
|
||||||
|
const StatusWrapper = styled('div')`
|
||||||
|
position: absolute;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const IndicatorWrapper = styled('div')`
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 20px;
|
||||||
|
min-height: 20px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const enter = keyframes`
|
||||||
|
from {
|
||||||
|
transform: scale(0.6);
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}`;
|
||||||
|
|
||||||
|
export const AnimatedIconWrapper = styled('div')`
|
||||||
|
position: relative;
|
||||||
|
transform: scale(0.6);
|
||||||
|
opacity: 0.4;
|
||||||
|
min-width: 20px;
|
||||||
|
animation: ${enter} 0.3s 0.12s cubic-bezier(0.175, 0.885, 0.32, 1.275)
|
||||||
|
forwards;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export type IconThemes = Partial<{
|
||||||
|
success: CheckmarkTheme;
|
||||||
|
error: ErrorTheme;
|
||||||
|
loading: LoaderTheme;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export const ToastIcon: React.FC<{
|
||||||
|
toast: Toast;
|
||||||
|
}> = ({ toast }) => {
|
||||||
|
const { icon, type, iconTheme } = toast;
|
||||||
|
if (icon !== undefined) {
|
||||||
|
if (typeof icon === 'string') {
|
||||||
|
return <AnimatedIconWrapper>{icon}</AnimatedIconWrapper>;
|
||||||
|
} else {
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'blank') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IndicatorWrapper>
|
||||||
|
<LoaderIcon {...iconTheme} />
|
||||||
|
{type !== 'loading' && (
|
||||||
|
<StatusWrapper>
|
||||||
|
{type === 'error' ? (
|
||||||
|
<ErrorIcon {...iconTheme} />
|
||||||
|
) : (
|
||||||
|
<CheckmarkIcon {...iconTheme} />
|
||||||
|
)}
|
||||||
|
</StatusWrapper>
|
||||||
|
)}
|
||||||
|
</IndicatorWrapper>
|
||||||
|
);
|
||||||
|
};
|
141
web/src/components/hot-toast/components/toaster.tsx
Normal file
141
web/src/components/hot-toast/components/toaster.tsx
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
import { css, setup } from 'goober';
|
||||||
|
import * as React from 'react';
|
||||||
|
import {
|
||||||
|
resolveValue,
|
||||||
|
ToasterProps,
|
||||||
|
ToastPosition,
|
||||||
|
ToastWrapperProps,
|
||||||
|
} from '../core/types';
|
||||||
|
import { useToaster } from '../core/use-toaster';
|
||||||
|
import { prefersReducedMotion } from '../core/utils';
|
||||||
|
import { ToastBar } from './toast-bar';
|
||||||
|
|
||||||
|
setup(React.createElement);
|
||||||
|
|
||||||
|
const ToastWrapper = ({
|
||||||
|
id,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
onHeightUpdate,
|
||||||
|
children,
|
||||||
|
}: ToastWrapperProps) => {
|
||||||
|
const ref = React.useCallback(
|
||||||
|
(el: HTMLElement | null) => {
|
||||||
|
if (el) {
|
||||||
|
const updateHeight = () => {
|
||||||
|
const height = el.getBoundingClientRect().height;
|
||||||
|
onHeightUpdate(id, height);
|
||||||
|
};
|
||||||
|
updateHeight();
|
||||||
|
new MutationObserver(updateHeight).observe(el, {
|
||||||
|
subtree: true,
|
||||||
|
childList: true,
|
||||||
|
characterData: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[id, onHeightUpdate]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} className={className} style={style}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPositionStyle = (
|
||||||
|
position: ToastPosition,
|
||||||
|
offset: number
|
||||||
|
): React.CSSProperties => {
|
||||||
|
const top = position.includes('top');
|
||||||
|
const verticalStyle: React.CSSProperties = top ? { top: 0 } : { bottom: 0 };
|
||||||
|
const horizontalStyle: React.CSSProperties = position.includes('center')
|
||||||
|
? {
|
||||||
|
justifyContent: 'center',
|
||||||
|
}
|
||||||
|
: position.includes('right')
|
||||||
|
? {
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
return {
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
display: 'flex',
|
||||||
|
position: 'absolute',
|
||||||
|
transition: prefersReducedMotion()
|
||||||
|
? undefined
|
||||||
|
: `all 230ms cubic-bezier(.21,1.02,.73,1)`,
|
||||||
|
transform: `translateY(${offset * (top ? 1 : -1)}px)`,
|
||||||
|
...verticalStyle,
|
||||||
|
...horizontalStyle,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const activeClass = css`
|
||||||
|
z-index: 9999;
|
||||||
|
> * {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const DEFAULT_OFFSET = 16;
|
||||||
|
|
||||||
|
export const Toaster: React.FC<ToasterProps> = ({
|
||||||
|
reverseOrder,
|
||||||
|
position = 'top-center',
|
||||||
|
toastOptions,
|
||||||
|
gutter,
|
||||||
|
children,
|
||||||
|
containerStyle,
|
||||||
|
containerClassName,
|
||||||
|
}) => {
|
||||||
|
const { toasts, handlers } = useToaster(toastOptions);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
zIndex: 9999,
|
||||||
|
top: DEFAULT_OFFSET,
|
||||||
|
left: DEFAULT_OFFSET,
|
||||||
|
right: DEFAULT_OFFSET,
|
||||||
|
bottom: DEFAULT_OFFSET,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
...containerStyle,
|
||||||
|
}}
|
||||||
|
className={containerClassName}
|
||||||
|
onMouseEnter={handlers.startPause}
|
||||||
|
onMouseLeave={handlers.endPause}
|
||||||
|
>
|
||||||
|
{toasts.map((t) => {
|
||||||
|
const toastPosition = t.position || position;
|
||||||
|
const offset = handlers.calculateOffset(t, {
|
||||||
|
reverseOrder,
|
||||||
|
gutter,
|
||||||
|
defaultPosition: position,
|
||||||
|
});
|
||||||
|
const positionStyle = getPositionStyle(toastPosition, offset);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToastWrapper
|
||||||
|
id={t.id}
|
||||||
|
key={t.id}
|
||||||
|
onHeightUpdate={handlers.updateHeight}
|
||||||
|
className={t.visible ? activeClass : ''}
|
||||||
|
style={positionStyle}
|
||||||
|
>
|
||||||
|
{t.type === 'custom' ? (
|
||||||
|
resolveValue(t.message, t)
|
||||||
|
) : children ? (
|
||||||
|
children(t)
|
||||||
|
) : (
|
||||||
|
<ToastBar toast={t} position={toastPosition} />
|
||||||
|
)}
|
||||||
|
</ToastWrapper>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
217
web/src/components/hot-toast/core/store.ts
Normal file
217
web/src/components/hot-toast/core/store.ts
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { DefaultToastOptions, Toast, ToastType } from './types';
|
||||||
|
|
||||||
|
const TOAST_LIMIT = 20;
|
||||||
|
|
||||||
|
export enum ActionType {
|
||||||
|
ADD_TOAST,
|
||||||
|
UPDATE_TOAST,
|
||||||
|
UPSERT_TOAST,
|
||||||
|
DISMISS_TOAST,
|
||||||
|
REMOVE_TOAST,
|
||||||
|
START_PAUSE,
|
||||||
|
END_PAUSE,
|
||||||
|
}
|
||||||
|
|
||||||
|
type Action =
|
||||||
|
| {
|
||||||
|
type: ActionType.ADD_TOAST;
|
||||||
|
toast: Toast;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType.UPSERT_TOAST;
|
||||||
|
toast: Toast;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType.UPDATE_TOAST;
|
||||||
|
toast: Partial<Toast>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType.DISMISS_TOAST;
|
||||||
|
toastId?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType.REMOVE_TOAST;
|
||||||
|
toastId?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType.START_PAUSE;
|
||||||
|
time: number;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType.END_PAUSE;
|
||||||
|
time: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
toasts: Toast[];
|
||||||
|
pausedAt: number | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toastTimeouts = new Map<Toast['id'], ReturnType<typeof setTimeout>>();
|
||||||
|
|
||||||
|
export const TOAST_EXPIRE_DISMISS_DELAY = 1000;
|
||||||
|
|
||||||
|
const addToRemoveQueue = (toastId: string) => {
|
||||||
|
if (toastTimeouts.has(toastId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
toastTimeouts.delete(toastId);
|
||||||
|
dispatch({
|
||||||
|
type: ActionType.REMOVE_TOAST,
|
||||||
|
toastId: toastId,
|
||||||
|
});
|
||||||
|
}, TOAST_EXPIRE_DISMISS_DELAY);
|
||||||
|
|
||||||
|
toastTimeouts.set(toastId, timeout);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearFromRemoveQueue = (toastId: string) => {
|
||||||
|
const timeout = toastTimeouts.get(toastId);
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const reducer = (state: State, action: Action): State => {
|
||||||
|
switch (action.type) {
|
||||||
|
case ActionType.ADD_TOAST:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||||
|
};
|
||||||
|
|
||||||
|
case ActionType.UPDATE_TOAST:
|
||||||
|
// ! Side effects !
|
||||||
|
if (action.toast.id) {
|
||||||
|
clearFromRemoveQueue(action.toast.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.map((t) =>
|
||||||
|
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
case ActionType.UPSERT_TOAST: {
|
||||||
|
const { toast } = action;
|
||||||
|
return state.toasts.find((t) => t.id === toast.id)
|
||||||
|
? reducer(state, { type: ActionType.UPDATE_TOAST, toast })
|
||||||
|
: reducer(state, { type: ActionType.ADD_TOAST, toast });
|
||||||
|
}
|
||||||
|
|
||||||
|
case ActionType.DISMISS_TOAST: {
|
||||||
|
const { toastId } = action;
|
||||||
|
|
||||||
|
// ! Side effects ! - This could be execrated into a dismissToast() action, but I'll keep it here for simplicity
|
||||||
|
if (toastId) {
|
||||||
|
addToRemoveQueue(toastId);
|
||||||
|
} else {
|
||||||
|
state.toasts.forEach((toast) => {
|
||||||
|
addToRemoveQueue(toast.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.map((t) =>
|
||||||
|
t.id === toastId || toastId === undefined
|
||||||
|
? {
|
||||||
|
...t,
|
||||||
|
visible: false,
|
||||||
|
}
|
||||||
|
: t
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case ActionType.REMOVE_TOAST:
|
||||||
|
if (action.toastId === undefined) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||||
|
};
|
||||||
|
|
||||||
|
case ActionType.START_PAUSE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
pausedAt: action.time,
|
||||||
|
};
|
||||||
|
|
||||||
|
case ActionType.END_PAUSE: {
|
||||||
|
const diff = action.time - (state.pausedAt || 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
pausedAt: undefined,
|
||||||
|
toasts: state.toasts.map((t) => ({
|
||||||
|
...t,
|
||||||
|
pauseDuration: t.pauseDuration + diff,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const listeners: Array<(state: State) => void> = [];
|
||||||
|
|
||||||
|
let memoryState: State = { toasts: [], pausedAt: undefined };
|
||||||
|
|
||||||
|
export const dispatch = (action: Action) => {
|
||||||
|
memoryState = reducer(memoryState, action);
|
||||||
|
listeners.forEach((listener) => {
|
||||||
|
listener(memoryState);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultTimeouts: {
|
||||||
|
[key in ToastType]: number;
|
||||||
|
} = {
|
||||||
|
blank: 4000,
|
||||||
|
error: 4000,
|
||||||
|
success: 2000,
|
||||||
|
loading: Infinity,
|
||||||
|
custom: 4000,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useStore = (toastOptions: DefaultToastOptions = {}): State => {
|
||||||
|
const [state, setState] = useState<State>(memoryState);
|
||||||
|
useEffect(() => {
|
||||||
|
listeners.push(setState);
|
||||||
|
return () => {
|
||||||
|
const index = listeners.indexOf(setState);
|
||||||
|
if (index > -1) {
|
||||||
|
listeners.splice(index, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [state]);
|
||||||
|
|
||||||
|
const mergedToasts = state.toasts.map((t) => ({
|
||||||
|
...toastOptions,
|
||||||
|
...toastOptions[t.type],
|
||||||
|
...t,
|
||||||
|
duration:
|
||||||
|
t.duration ||
|
||||||
|
toastOptions[t.type]?.duration ||
|
||||||
|
toastOptions?.duration ||
|
||||||
|
defaultTimeouts[t.type],
|
||||||
|
style: {
|
||||||
|
...toastOptions.style,
|
||||||
|
...toastOptions[t.type]?.style,
|
||||||
|
...t.style,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: mergedToasts,
|
||||||
|
};
|
||||||
|
};
|
92
web/src/components/hot-toast/core/toast.ts
Normal file
92
web/src/components/hot-toast/core/toast.ts
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
import {
|
||||||
|
Renderable,
|
||||||
|
Toast,
|
||||||
|
ToastOptions,
|
||||||
|
ToastType,
|
||||||
|
DefaultToastOptions,
|
||||||
|
ValueOrFunction,
|
||||||
|
resolveValue,
|
||||||
|
} from './types';
|
||||||
|
import { genId } from './utils';
|
||||||
|
import { dispatch, ActionType } from './store';
|
||||||
|
|
||||||
|
type Message = ValueOrFunction<Renderable, Toast>;
|
||||||
|
|
||||||
|
type ToastHandler = (message: Message, options?: ToastOptions) => string;
|
||||||
|
|
||||||
|
const createToast = (
|
||||||
|
message: Message,
|
||||||
|
type: ToastType = 'blank',
|
||||||
|
opts?: ToastOptions
|
||||||
|
): Toast => ({
|
||||||
|
createdAt: Date.now(),
|
||||||
|
visible: true,
|
||||||
|
type,
|
||||||
|
ariaProps: {
|
||||||
|
role: 'status',
|
||||||
|
'aria-live': 'polite',
|
||||||
|
},
|
||||||
|
message,
|
||||||
|
pauseDuration: 0,
|
||||||
|
...opts,
|
||||||
|
id: opts?.id || genId(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const createHandler =
|
||||||
|
(type?: ToastType): ToastHandler =>
|
||||||
|
(message, options) => {
|
||||||
|
const toast = createToast(message, type, options);
|
||||||
|
dispatch({ type: ActionType.UPSERT_TOAST, toast });
|
||||||
|
return toast.id;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toast = (message: Message, opts?: ToastOptions) =>
|
||||||
|
createHandler('blank')(message, opts);
|
||||||
|
|
||||||
|
toast.error = createHandler('error');
|
||||||
|
toast.success = createHandler('success');
|
||||||
|
toast.loading = createHandler('loading');
|
||||||
|
toast.custom = createHandler('custom');
|
||||||
|
|
||||||
|
toast.dismiss = (toastId?: string) => {
|
||||||
|
dispatch({
|
||||||
|
type: ActionType.DISMISS_TOAST,
|
||||||
|
toastId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
toast.remove = (toastId?: string) =>
|
||||||
|
dispatch({ type: ActionType.REMOVE_TOAST, toastId });
|
||||||
|
|
||||||
|
toast.promise = <T>(
|
||||||
|
promise: Promise<T>,
|
||||||
|
msgs: {
|
||||||
|
loading: Renderable;
|
||||||
|
success: ValueOrFunction<Renderable, T>;
|
||||||
|
error: ValueOrFunction<Renderable, unknown>;
|
||||||
|
},
|
||||||
|
opts?: DefaultToastOptions
|
||||||
|
) => {
|
||||||
|
const id = toast.loading(msgs.loading, { ...opts, ...opts?.loading });
|
||||||
|
|
||||||
|
promise
|
||||||
|
.then((p) => {
|
||||||
|
toast.success(resolveValue(msgs.success, p), {
|
||||||
|
id,
|
||||||
|
...opts,
|
||||||
|
...opts?.success,
|
||||||
|
});
|
||||||
|
return p;
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
toast.error(resolveValue(msgs.error, e), {
|
||||||
|
id,
|
||||||
|
...opts,
|
||||||
|
...opts?.error,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { toast };
|
91
web/src/components/hot-toast/core/types.ts
Normal file
91
web/src/components/hot-toast/core/types.ts
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
import React, { CSSProperties } from 'react';
|
||||||
|
|
||||||
|
export type ToastType = 'success' | 'error' | 'loading' | 'blank' | 'custom';
|
||||||
|
export type ToastPosition =
|
||||||
|
| 'top-left'
|
||||||
|
| 'top-center'
|
||||||
|
| 'top-right'
|
||||||
|
| 'bottom-left'
|
||||||
|
| 'bottom-center'
|
||||||
|
| 'bottom-right';
|
||||||
|
|
||||||
|
export type Renderable = React.JSX.Element | string | null;
|
||||||
|
|
||||||
|
export interface IconTheme {
|
||||||
|
primary: string;
|
||||||
|
secondary: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ValueFunction<TValue, TArg> = (arg: TArg) => TValue;
|
||||||
|
export type ValueOrFunction<TValue, TArg> =
|
||||||
|
| TValue
|
||||||
|
| ValueFunction<TValue, TArg>;
|
||||||
|
|
||||||
|
const isFunction = <TValue, TArg>(
|
||||||
|
valOrFunction: ValueOrFunction<TValue, TArg>
|
||||||
|
): valOrFunction is ValueFunction<TValue, TArg> =>
|
||||||
|
typeof valOrFunction === 'function';
|
||||||
|
|
||||||
|
export const resolveValue = <TValue, TArg>(
|
||||||
|
valOrFunction: ValueOrFunction<TValue, TArg>,
|
||||||
|
arg: TArg
|
||||||
|
): TValue => (isFunction(valOrFunction) ? valOrFunction(arg) : valOrFunction);
|
||||||
|
|
||||||
|
export interface Toast {
|
||||||
|
type: ToastType;
|
||||||
|
id: string;
|
||||||
|
message: ValueOrFunction<Renderable, Toast>;
|
||||||
|
icon?: Renderable;
|
||||||
|
duration?: number;
|
||||||
|
pauseDuration: number;
|
||||||
|
position?: ToastPosition;
|
||||||
|
|
||||||
|
ariaProps: {
|
||||||
|
role: 'status' | 'alert';
|
||||||
|
'aria-live': 'assertive' | 'off' | 'polite';
|
||||||
|
};
|
||||||
|
|
||||||
|
style?: CSSProperties;
|
||||||
|
className?: string;
|
||||||
|
iconTheme?: IconTheme;
|
||||||
|
|
||||||
|
createdAt: number;
|
||||||
|
visible: boolean;
|
||||||
|
height?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ToastOptions = Partial<
|
||||||
|
Pick<
|
||||||
|
Toast,
|
||||||
|
| 'id'
|
||||||
|
| 'icon'
|
||||||
|
| 'duration'
|
||||||
|
| 'ariaProps'
|
||||||
|
| 'className'
|
||||||
|
| 'style'
|
||||||
|
| 'position'
|
||||||
|
| 'iconTheme'
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type DefaultToastOptions = ToastOptions & {
|
||||||
|
[key in ToastType]?: ToastOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ToasterProps {
|
||||||
|
position?: ToastPosition;
|
||||||
|
toastOptions?: DefaultToastOptions;
|
||||||
|
reverseOrder?: boolean;
|
||||||
|
gutter?: number;
|
||||||
|
containerStyle?: React.CSSProperties;
|
||||||
|
containerClassName?: string;
|
||||||
|
children?: (toast: Toast) => React.JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToastWrapperProps {
|
||||||
|
id: string;
|
||||||
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
onHeightUpdate: (id: string, height: number) => void;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
96
web/src/components/hot-toast/core/use-toaster.ts
Normal file
96
web/src/components/hot-toast/core/use-toaster.ts
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
import { useEffect, useCallback } from 'react';
|
||||||
|
import { dispatch, ActionType, useStore } from './store';
|
||||||
|
import { toast } from './toast';
|
||||||
|
import { DefaultToastOptions, Toast, ToastPosition } from './types';
|
||||||
|
|
||||||
|
const updateHeight = (toastId: string, height: number) => {
|
||||||
|
dispatch({
|
||||||
|
type: ActionType.UPDATE_TOAST,
|
||||||
|
toast: { id: toastId, height },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const startPause = () => {
|
||||||
|
dispatch({
|
||||||
|
type: ActionType.START_PAUSE,
|
||||||
|
time: Date.now(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useToaster = (toastOptions?: DefaultToastOptions) => {
|
||||||
|
const { toasts, pausedAt } = useStore(toastOptions);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (pausedAt) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const timeouts = toasts.map((t) => {
|
||||||
|
if (t.duration === Infinity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const durationLeft =
|
||||||
|
(t.duration || 0) + t.pauseDuration - (now - t.createdAt);
|
||||||
|
|
||||||
|
if (durationLeft < 0) {
|
||||||
|
if (t.visible) {
|
||||||
|
toast.dismiss(t.id);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return setTimeout(() => toast.dismiss(t.id), durationLeft);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
timeouts.forEach((timeout) => timeout && clearTimeout(timeout));
|
||||||
|
};
|
||||||
|
}, [toasts, pausedAt]);
|
||||||
|
|
||||||
|
const endPause = useCallback(() => {
|
||||||
|
if (pausedAt) {
|
||||||
|
dispatch({ type: ActionType.END_PAUSE, time: Date.now() });
|
||||||
|
}
|
||||||
|
}, [pausedAt]);
|
||||||
|
|
||||||
|
const calculateOffset = useCallback(
|
||||||
|
(
|
||||||
|
toast: Toast,
|
||||||
|
opts?: {
|
||||||
|
reverseOrder?: boolean;
|
||||||
|
gutter?: number;
|
||||||
|
defaultPosition?: ToastPosition;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const { reverseOrder = false, gutter = 8, defaultPosition } = opts || {};
|
||||||
|
|
||||||
|
const relevantToasts = toasts.filter(
|
||||||
|
(t) =>
|
||||||
|
(t.position || defaultPosition) ===
|
||||||
|
(toast.position || defaultPosition) && t.height
|
||||||
|
);
|
||||||
|
const toastIndex = relevantToasts.findIndex((t) => t.id === toast.id);
|
||||||
|
const toastsBefore = relevantToasts.filter(
|
||||||
|
(toast, i) => i < toastIndex && toast.visible
|
||||||
|
).length;
|
||||||
|
|
||||||
|
const offset = relevantToasts
|
||||||
|
.filter((t) => t.visible)
|
||||||
|
.slice(...(reverseOrder ? [toastsBefore + 1] : [0, toastsBefore]))
|
||||||
|
.reduce((acc, t) => acc + (t.height || 0) + gutter, 0);
|
||||||
|
|
||||||
|
return offset;
|
||||||
|
},
|
||||||
|
[toasts]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
toasts,
|
||||||
|
handlers: {
|
||||||
|
updateHeight,
|
||||||
|
startPause,
|
||||||
|
endPause,
|
||||||
|
calculateOffset,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
19
web/src/components/hot-toast/core/utils.ts
Normal file
19
web/src/components/hot-toast/core/utils.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
export const genId = (() => {
|
||||||
|
let count = 0;
|
||||||
|
return () => {
|
||||||
|
return (++count).toString();
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
export const prefersReducedMotion = (() => {
|
||||||
|
// Cache result
|
||||||
|
let shouldReduceMotion: boolean | undefined = undefined;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (shouldReduceMotion === undefined && typeof window !== 'undefined') {
|
||||||
|
const mediaQuery = matchMedia('(prefers-reduced-motion: reduce)');
|
||||||
|
shouldReduceMotion = !mediaQuery || mediaQuery.matches;
|
||||||
|
}
|
||||||
|
return shouldReduceMotion;
|
||||||
|
};
|
||||||
|
})();
|
21
web/src/components/hot-toast/headless/index.ts
Normal file
21
web/src/components/hot-toast/headless/index.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { toast } from '../core/toast';
|
||||||
|
|
||||||
|
export type {
|
||||||
|
DefaultToastOptions,
|
||||||
|
IconTheme,
|
||||||
|
Renderable,
|
||||||
|
Toast,
|
||||||
|
ToasterProps,
|
||||||
|
ToastOptions,
|
||||||
|
ToastPosition,
|
||||||
|
ToastType,
|
||||||
|
ValueFunction,
|
||||||
|
ValueOrFunction,
|
||||||
|
} from '../core/types';
|
||||||
|
|
||||||
|
export { resolveValue } from '../core/types';
|
||||||
|
export { useToaster } from '../core/use-toaster';
|
||||||
|
export { useStore as useToasterStore } from '../core/store';
|
||||||
|
|
||||||
|
export { toast };
|
||||||
|
export default toast;
|
13
web/src/components/hot-toast/index.ts
Normal file
13
web/src/components/hot-toast/index.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { toast } from './core/toast';
|
||||||
|
|
||||||
|
export * from './headless';
|
||||||
|
|
||||||
|
export { ToastBar } from './components/toast-bar';
|
||||||
|
export { ToastIcon } from './components/toast-icon';
|
||||||
|
export { Toaster } from './components/toaster';
|
||||||
|
export { CheckmarkIcon } from './components/checkmark';
|
||||||
|
export { ErrorIcon } from './components/error';
|
||||||
|
export { LoaderIcon } from './components/loader';
|
||||||
|
|
||||||
|
export { toast };
|
||||||
|
export default toast;
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { CheckCircleIcon, ExclamationCircleIcon, ExclamationTriangleIcon, InformationCircleIcon, XMarkIcon } from "@heroicons/react/24/solid";
|
import { CheckCircleIcon, ExclamationCircleIcon, ExclamationTriangleIcon, InformationCircleIcon, XMarkIcon } from "@heroicons/react/24/solid";
|
||||||
import { toast, Toast as Tooast } from "react-hot-toast";
|
import { toast, Toast as Tooast } from "@components/hot-toast";
|
||||||
import { classNames } from "@utils";
|
import { classNames } from "@utils";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
import { Fragment, useRef } from "react";
|
import { Fragment, useRef } from "react";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
import { XMarkIcon } from "@heroicons/react/24/solid";
|
import { XMarkIcon } from "@heroicons/react/24/solid";
|
||||||
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from "@headlessui/react";
|
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from "@headlessui/react";
|
||||||
import type { FieldProps } from "formik";
|
import type { FieldProps } from "formik";
|
||||||
|
@ -15,6 +14,7 @@ import { Field, Form, Formik, FormikErrors, FormikValues } from "formik";
|
||||||
import { APIClient } from "@api/APIClient";
|
import { APIClient } from "@api/APIClient";
|
||||||
import { FilterKeys } from "@api/query_keys";
|
import { FilterKeys } from "@api/query_keys";
|
||||||
import { DEBUG } from "@components/debug";
|
import { DEBUG } from "@components/debug";
|
||||||
|
import { toast } from "@components/hot-toast";
|
||||||
import Toast from "@components/notifications/Toast";
|
import Toast from "@components/notifications/Toast";
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
|
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
import { XMarkIcon } from "@heroicons/react/24/solid";
|
import { XMarkIcon } from "@heroicons/react/24/solid";
|
||||||
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from "@headlessui/react";
|
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from "@headlessui/react";
|
||||||
import type { FieldProps } from "formik";
|
import type { FieldProps } from "formik";
|
||||||
|
@ -14,6 +13,7 @@ import { Field, Form, Formik, FormikErrors, FormikValues } from "formik";
|
||||||
import { APIClient } from "@api/APIClient";
|
import { APIClient } from "@api/APIClient";
|
||||||
import { ApiKeys } from "@api/query_keys";
|
import { ApiKeys } from "@api/query_keys";
|
||||||
import { DEBUG } from "@components/debug";
|
import { DEBUG } from "@components/debug";
|
||||||
|
import { toast } from "@components/hot-toast";
|
||||||
import Toast from "@components/notifications/Toast";
|
import Toast from "@components/notifications/Toast";
|
||||||
|
|
||||||
interface apiKeyAddFormProps {
|
interface apiKeyAddFormProps {
|
||||||
|
|
|
@ -8,13 +8,13 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from "@headlessui/react";
|
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from "@headlessui/react";
|
||||||
import { XMarkIcon } from "@heroicons/react/24/solid";
|
import { XMarkIcon } from "@heroicons/react/24/solid";
|
||||||
import { Form, Formik, useFormikContext } from "formik";
|
import { Form, Formik, useFormikContext } from "formik";
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
|
|
||||||
import { classNames, sleep } from "@utils";
|
import { classNames, sleep } from "@utils";
|
||||||
import { DEBUG } from "@components/debug";
|
import { DEBUG } from "@components/debug";
|
||||||
import { APIClient } from "@api/APIClient";
|
import { APIClient } from "@api/APIClient";
|
||||||
import { DownloadClientKeys } from "@api/query_keys";
|
import { DownloadClientKeys } from "@api/query_keys";
|
||||||
import { DownloadClientAuthType, DownloadClientTypeOptions, DownloadRuleConditionOptions } from "@domain/constants";
|
import { DownloadClientAuthType, DownloadClientTypeOptions, DownloadRuleConditionOptions } from "@domain/constants";
|
||||||
|
import { toast } from "@components/hot-toast";
|
||||||
import Toast from "@components/notifications/Toast";
|
import Toast from "@components/notifications/Toast";
|
||||||
import { useToggle } from "@hooks/hooks";
|
import { useToggle } from "@hooks/hooks";
|
||||||
import { DeleteModal } from "@components/modals";
|
import { DeleteModal } from "@components/modals";
|
||||||
|
|
|
@ -5,11 +5,11 @@
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
import { useFormikContext } from "formik";
|
import { useFormikContext } from "formik";
|
||||||
|
|
||||||
import { APIClient } from "@api/APIClient";
|
import { APIClient } from "@api/APIClient";
|
||||||
import { FeedKeys } from "@api/query_keys";
|
import { FeedKeys } from "@api/query_keys";
|
||||||
|
import { toast } from "@components/hot-toast";
|
||||||
import Toast from "@components/notifications/Toast";
|
import Toast from "@components/notifications/Toast";
|
||||||
import { SlideOver } from "@components/panels";
|
import { SlideOver } from "@components/panels";
|
||||||
import { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "@components/inputs";
|
import { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "@components/inputs";
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Fragment, useState } from "react";
|
import { Fragment, useState } from "react";
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import Select from "react-select";
|
import Select from "react-select";
|
||||||
import type { FieldProps } from "formik";
|
import type { FieldProps } from "formik";
|
||||||
|
@ -18,6 +17,7 @@ import { APIClient } from "@api/APIClient";
|
||||||
import { FeedKeys, IndexerKeys, ReleaseKeys } from "@api/query_keys";
|
import { FeedKeys, IndexerKeys, ReleaseKeys } from "@api/query_keys";
|
||||||
import { IndexersSchemaQueryOptions, ProxiesQueryOptions } from "@api/queries";
|
import { IndexersSchemaQueryOptions, ProxiesQueryOptions } from "@api/queries";
|
||||||
import { SlideOver } from "@components/panels";
|
import { SlideOver } from "@components/panels";
|
||||||
|
import { toast } from "@components/hot-toast";
|
||||||
import Toast from "@components/notifications/Toast";
|
import Toast from "@components/notifications/Toast";
|
||||||
import { PasswordFieldWide, SwitchButton, SwitchGroupWide, TextFieldWide } from "@components/inputs";
|
import { PasswordFieldWide, SwitchButton, SwitchGroupWide, TextFieldWide } from "@components/inputs";
|
||||||
import { SelectFieldBasic, SelectFieldCreatable } from "@components/inputs/select_wide";
|
import { SelectFieldBasic, SelectFieldCreatable } from "@components/inputs/select_wide";
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
import { XMarkIcon } from "@heroicons/react/24/solid";
|
import { XMarkIcon } from "@heroicons/react/24/solid";
|
||||||
import type { FieldProps } from "formik";
|
import type { FieldProps } from "formik";
|
||||||
import type { FieldArrayRenderProps } from "formik";
|
import type { FieldArrayRenderProps } from "formik";
|
||||||
|
@ -18,6 +17,7 @@ import { APIClient } from "@api/APIClient";
|
||||||
import { IrcKeys } from "@api/query_keys";
|
import { IrcKeys } from "@api/query_keys";
|
||||||
import { NumberFieldWide, PasswordFieldWide, SwitchButton, SwitchGroupWide, TextFieldWide } from "@components/inputs";
|
import { NumberFieldWide, PasswordFieldWide, SwitchButton, SwitchGroupWide, TextFieldWide } from "@components/inputs";
|
||||||
import { SlideOver } from "@components/panels";
|
import { SlideOver } from "@components/panels";
|
||||||
|
import { toast } from "@components/hot-toast";
|
||||||
import Toast from "@components/notifications/Toast";
|
import Toast from "@components/notifications/Toast";
|
||||||
import * as common from "@components/inputs/common";
|
import * as common from "@components/inputs/common";
|
||||||
import { classNames } from "@utils";
|
import { classNames } from "@utils";
|
||||||
|
|
|
@ -10,7 +10,6 @@ import { Field, Form, Formik, FormikErrors, FormikValues } from "formik";
|
||||||
import { XMarkIcon } from "@heroicons/react/24/solid";
|
import { XMarkIcon } from "@heroicons/react/24/solid";
|
||||||
import Select from "react-select";
|
import Select from "react-select";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
|
|
||||||
import { APIClient } from "@api/APIClient";
|
import { APIClient } from "@api/APIClient";
|
||||||
import { NotificationKeys } from "@api/query_keys";
|
import { NotificationKeys } from "@api/query_keys";
|
||||||
|
@ -18,6 +17,7 @@ import { EventOptions, NotificationTypeOptions, SelectOption } from "@domain/con
|
||||||
import { DEBUG } from "@components/debug";
|
import { DEBUG } from "@components/debug";
|
||||||
import { SlideOver } from "@components/panels";
|
import { SlideOver } from "@components/panels";
|
||||||
import { ExternalLink } from "@components/ExternalLink";
|
import { ExternalLink } from "@components/ExternalLink";
|
||||||
|
import { toast } from "@components/hot-toast";
|
||||||
import Toast from "@components/notifications/Toast";
|
import Toast from "@components/notifications/Toast";
|
||||||
import * as common from "@components/inputs/common";
|
import * as common from "@components/inputs/common";
|
||||||
import { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "@components/inputs";
|
import { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "@components/inputs";
|
||||||
|
|
|
@ -8,7 +8,6 @@ import { Form, Formik, FormikValues } from "formik";
|
||||||
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from "@headlessui/react";
|
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from "@headlessui/react";
|
||||||
import { XMarkIcon } from "@heroicons/react/24/solid";
|
import { XMarkIcon } from "@heroicons/react/24/solid";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
|
|
||||||
import { AddProps } from "@forms/settings/IndexerForms";
|
import { AddProps } from "@forms/settings/IndexerForms";
|
||||||
import { DEBUG } from "@components/debug.tsx";
|
import { DEBUG } from "@components/debug.tsx";
|
||||||
|
@ -17,6 +16,7 @@ import { SelectFieldBasic } from "@components/inputs/select_wide";
|
||||||
import { ProxyTypeOptions } from "@domain/constants";
|
import { ProxyTypeOptions } from "@domain/constants";
|
||||||
import { APIClient } from "@api/APIClient";
|
import { APIClient } from "@api/APIClient";
|
||||||
import { ProxyKeys } from "@api/query_keys";
|
import { ProxyKeys } from "@api/query_keys";
|
||||||
|
import { toast } from "@components/hot-toast";
|
||||||
import Toast from "@components/notifications/Toast";
|
import Toast from "@components/notifications/Toast";
|
||||||
import { SlideOver } from "@components/panels";
|
import { SlideOver } from "@components/panels";
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,6 @@ import {
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
import { ExclamationCircleIcon } from "@heroicons/react/24/solid";
|
import { ExclamationCircleIcon } from "@heroicons/react/24/solid";
|
||||||
import { format } from "date-fns/format";
|
import { format } from "date-fns/format";
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
|
|
||||||
import { APIClient } from "@api/APIClient";
|
import { APIClient } from "@api/APIClient";
|
||||||
import { Checkbox } from "@components/Checkbox";
|
import { Checkbox } from "@components/Checkbox";
|
||||||
|
@ -21,6 +20,7 @@ import { baseUrl, classNames, simplifyDate } from "@utils";
|
||||||
import { SettingsContext } from "@utils/Context";
|
import { SettingsContext } from "@utils/Context";
|
||||||
import { EmptySimple } from "@components/emptystates";
|
import { EmptySimple } from "@components/emptystates";
|
||||||
import { RingResizeSpinner } from "@components/Icons";
|
import { RingResizeSpinner } from "@components/Icons";
|
||||||
|
import { toast } from "@components/hot-toast";
|
||||||
import Toast from "@components/notifications/Toast";
|
import Toast from "@components/notifications/Toast";
|
||||||
|
|
||||||
type LogEvent = {
|
type LogEvent = {
|
||||||
|
|
|
@ -7,13 +7,13 @@ import React, { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { useMutation, useQuery, useQueryErrorResetBoundary } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryErrorResetBoundary } from "@tanstack/react-query";
|
||||||
import { getRouteApi, useRouter } from "@tanstack/react-router";
|
import { getRouteApi, useRouter } from "@tanstack/react-router";
|
||||||
import toast from "react-hot-toast";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faOpenid } from "@fortawesome/free-brands-svg-icons";
|
import { faOpenid } from "@fortawesome/free-brands-svg-icons";
|
||||||
|
|
||||||
import { RocketLaunchIcon } from "@heroicons/react/24/outline";
|
import { RocketLaunchIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
import { APIClient } from "@api/APIClient";
|
import { APIClient } from "@api/APIClient";
|
||||||
|
import toast from "@components/hot-toast";
|
||||||
import Toast from "@components/notifications/Toast";
|
import Toast from "@components/notifications/Toast";
|
||||||
import { Tooltip } from "@components/tooltips/Tooltip";
|
import { Tooltip } from "@components/tooltips/Tooltip";
|
||||||
import { PasswordInput, TextInput } from "@components/inputs/text";
|
import { PasswordInput, TextInput } from "@components/inputs/text";
|
||||||
|
|
|
@ -9,7 +9,6 @@ import { getRouteApi, Link, Outlet, useNavigate } from "@tanstack/react-router";
|
||||||
import { Form, Formik, useFormikContext } from "formik";
|
import { Form, Formik, useFormikContext } from "formik";
|
||||||
import type { FormikErrors, FormikValues } from "formik";
|
import type { FormikErrors, FormikValues } from "formik";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
import { toFormikValidationSchema } from "zod-formik-adapter";
|
import { toFormikValidationSchema } from "zod-formik-adapter";
|
||||||
import { ChevronRightIcon } from "@heroicons/react/24/solid";
|
import { ChevronRightIcon } from "@heroicons/react/24/solid";
|
||||||
|
|
||||||
|
@ -21,6 +20,7 @@ import { classNames } from "@utils";
|
||||||
import { DOWNLOAD_CLIENTS } from "@domain/constants";
|
import { DOWNLOAD_CLIENTS } from "@domain/constants";
|
||||||
|
|
||||||
import { DEBUG } from "@components/debug";
|
import { DEBUG } from "@components/debug";
|
||||||
|
import { toast } from "@components/hot-toast";
|
||||||
import Toast from "@components/notifications/Toast";
|
import Toast from "@components/notifications/Toast";
|
||||||
import { DeleteModal } from "@components/modals";
|
import { DeleteModal } from "@components/modals";
|
||||||
|
|
||||||
|
|
|
@ -6,10 +6,10 @@
|
||||||
import { Fragment, useRef, useState } from "react";
|
import { Fragment, useRef, useState } from "react";
|
||||||
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from "@headlessui/react";
|
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from "@headlessui/react";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import toast from "react-hot-toast";
|
|
||||||
|
|
||||||
import { APIClient } from "@api/APIClient";
|
import { APIClient } from "@api/APIClient";
|
||||||
import { FilterKeys } from "@api/query_keys";
|
import { FilterKeys } from "@api/query_keys";
|
||||||
|
import toast from "@components/hot-toast";
|
||||||
import Toast from "@components/notifications/Toast";
|
import Toast from "@components/notifications/Toast";
|
||||||
|
|
||||||
import { AutodlIrssiConfigParser } from "./_configParser";
|
import { AutodlIrssiConfigParser } from "./_configParser";
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
|
|
||||||
import { Dispatch, FC, Fragment, MouseEventHandler, useCallback, useEffect, useReducer, useRef, useState } from "react";
|
import { Dispatch, FC, Fragment, MouseEventHandler, useCallback, useEffect, useReducer, useRef, useState } from "react";
|
||||||
import { Link } from '@tanstack/react-router'
|
import { Link } from '@tanstack/react-router'
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
import {
|
import {
|
||||||
Listbox,
|
Listbox,
|
||||||
ListboxButton,
|
ListboxButton,
|
||||||
|
@ -38,6 +37,7 @@ import { useToggle } from "@hooks/hooks";
|
||||||
import { APIClient } from "@api/APIClient";
|
import { APIClient } from "@api/APIClient";
|
||||||
import { FilterKeys } from "@api/query_keys";
|
import { FilterKeys } from "@api/query_keys";
|
||||||
import { FiltersQueryOptions, IndexersOptionsQueryOptions } from "@api/queries";
|
import { FiltersQueryOptions, IndexersOptionsQueryOptions } from "@api/queries";
|
||||||
|
import { toast } from "@components/hot-toast";
|
||||||
import Toast from "@components/notifications/Toast";
|
import Toast from "@components/notifications/Toast";
|
||||||
import { EmptyListState } from "@components/emptystates";
|
import { EmptyListState } from "@components/emptystates";
|
||||||
import { DeleteModal } from "@components/modals";
|
import { DeleteModal } from "@components/modals";
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { Field, FieldArray, useFormikContext } from "formik";
|
import { Field, FieldArray, useFormikContext } from "formik";
|
||||||
import type { FieldProps, FieldArrayRenderProps } from "formik";
|
import type { FieldProps, FieldArrayRenderProps } from "formik";
|
||||||
|
@ -18,6 +17,7 @@ import { ActionTypeNameMap, ActionTypeOptions, DOWNLOAD_CLIENTS } from "@domain/
|
||||||
import { Select, TextField } from "@components/inputs";
|
import { Select, TextField } from "@components/inputs";
|
||||||
import { DeleteModal } from "@components/modals";
|
import { DeleteModal } from "@components/modals";
|
||||||
import { EmptyListState } from "@components/emptystates";
|
import { EmptyListState } from "@components/emptystates";
|
||||||
|
import { toast } from "@components/hot-toast";
|
||||||
import Toast from "@components/notifications/Toast";
|
import Toast from "@components/notifications/Toast";
|
||||||
|
|
||||||
import { Checkbox } from "@components/Checkbox";
|
import { Checkbox } from "@components/Checkbox";
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
|
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { Form, Formik } from "formik";
|
import { Form, Formik } from "formik";
|
||||||
import toast from "react-hot-toast";
|
|
||||||
import { UserIcon } from "@heroicons/react/24/solid";
|
import { UserIcon } from "@heroicons/react/24/solid";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faOpenid } from "@fortawesome/free-brands-svg-icons";
|
import { faOpenid } from "@fortawesome/free-brands-svg-icons";
|
||||||
|
@ -13,6 +12,7 @@ import { faOpenid } from "@fortawesome/free-brands-svg-icons";
|
||||||
import { APIClient } from "@api/APIClient";
|
import { APIClient } from "@api/APIClient";
|
||||||
import { Section } from "./_components";
|
import { Section } from "./_components";
|
||||||
import { PasswordField, TextField } from "@components/inputs";
|
import { PasswordField, TextField } from "@components/inputs";
|
||||||
|
import toast from "@components/hot-toast";
|
||||||
import Toast from "@components/notifications/Toast";
|
import Toast from "@components/notifications/Toast";
|
||||||
import { AuthContext } from "@utils/Context";
|
import { AuthContext } from "@utils/Context";
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,12 @@
|
||||||
|
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
|
import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
import { TrashIcon } from "@heroicons/react/24/outline";
|
import { TrashIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
import { KeyField } from "@components/fields/text";
|
import { KeyField } from "@components/fields/text";
|
||||||
import { DeleteModal } from "@components/modals";
|
import { DeleteModal } from "@components/modals";
|
||||||
import { APIKeyAddForm } from "@forms/settings/APIKeyAddForm";
|
import { APIKeyAddForm } from "@forms/settings/APIKeyAddForm";
|
||||||
|
import { toast } from "@components/hot-toast";
|
||||||
import Toast from "@components/notifications/Toast";
|
import Toast from "@components/notifications/Toast";
|
||||||
import { APIClient } from "@api/APIClient";
|
import { APIClient } from "@api/APIClient";
|
||||||
import { ApikeysQueryOptions } from "@api/queries";
|
import { ApikeysQueryOptions } from "@api/queries";
|
||||||
|
|
|
@ -5,13 +5,13 @@
|
||||||
|
|
||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { getRouteApi } from "@tanstack/react-router";
|
import { getRouteApi } from "@tanstack/react-router";
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
|
|
||||||
import { APIClient } from "@api/APIClient";
|
import { APIClient } from "@api/APIClient";
|
||||||
import { ConfigQueryOptions, UpdatesQueryOptions } from "@api/queries";
|
import { ConfigQueryOptions, UpdatesQueryOptions } from "@api/queries";
|
||||||
import { SettingsKeys } from "@api/query_keys";
|
import { SettingsKeys } from "@api/query_keys";
|
||||||
import { SettingsContext } from "@utils/Context";
|
import { SettingsContext } from "@utils/Context";
|
||||||
import { Checkbox } from "@components/Checkbox";
|
import { Checkbox } from "@components/Checkbox";
|
||||||
|
import { toast } from "@components/hot-toast";
|
||||||
import Toast from "@components/notifications/Toast";
|
import Toast from "@components/notifications/Toast";
|
||||||
import { ExternalLink } from "@components/ExternalLink";
|
import { ExternalLink } from "@components/ExternalLink";
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
|
import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { PlusIcon } from "@heroicons/react/24/solid";
|
import { PlusIcon } from "@heroicons/react/24/solid";
|
||||||
import toast from "react-hot-toast";
|
|
||||||
|
|
||||||
import { useToggle } from "@hooks/hooks";
|
import { useToggle } from "@hooks/hooks";
|
||||||
import { DownloadClientAddForm, DownloadClientUpdateForm } from "@forms";
|
import { DownloadClientAddForm, DownloadClientUpdateForm } from "@forms";
|
||||||
|
@ -15,6 +14,7 @@ import { APIClient } from "@api/APIClient";
|
||||||
import { DownloadClientKeys } from "@api/query_keys";
|
import { DownloadClientKeys } from "@api/query_keys";
|
||||||
import { DownloadClientsQueryOptions } from "@api/queries";
|
import { DownloadClientsQueryOptions } from "@api/queries";
|
||||||
import { ActionTypeNameMap } from "@domain/constants";
|
import { ActionTypeNameMap } from "@domain/constants";
|
||||||
|
import toast from "@components/hot-toast";
|
||||||
import Toast from "@components/notifications/Toast";
|
import Toast from "@components/notifications/Toast";
|
||||||
import { Checkbox } from "@components/Checkbox";
|
import { Checkbox } from "@components/Checkbox";
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
import { Fragment, useMemo, useRef, useState } from "react";
|
import { Fragment, useMemo, useRef, useState } from "react";
|
||||||
import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
|
import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { Menu, MenuButton, MenuItem, MenuItems, Transition } from "@headlessui/react";
|
import { Menu, MenuButton, MenuItem, MenuItems, Transition } from "@headlessui/react";
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
import {
|
import {
|
||||||
ArrowsRightLeftIcon,
|
ArrowsRightLeftIcon,
|
||||||
DocumentTextIcon,
|
DocumentTextIcon,
|
||||||
|
@ -21,6 +20,7 @@ import { FeedsQueryOptions } from "@api/queries";
|
||||||
import { FeedKeys } from "@api/query_keys";
|
import { FeedKeys } from "@api/query_keys";
|
||||||
import { useToggle } from "@hooks/hooks";
|
import { useToggle } from "@hooks/hooks";
|
||||||
import { baseUrl, classNames, IsEmptyDate, simplifyDate } from "@utils";
|
import { baseUrl, classNames, IsEmptyDate, simplifyDate } from "@utils";
|
||||||
|
import { toast } from "@components/hot-toast";
|
||||||
import Toast from "@components/notifications/Toast";
|
import Toast from "@components/notifications/Toast";
|
||||||
import { DeleteModal, ForceRunModal } from "@components/modals";
|
import { DeleteModal, ForceRunModal } from "@components/modals";
|
||||||
import { FeedUpdateForm } from "@forms/settings/FeedForms";
|
import { FeedUpdateForm } from "@forms/settings/FeedForms";
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
|
||||||
import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
|
import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { PlusIcon } from "@heroicons/react/24/solid";
|
import { PlusIcon } from "@heroicons/react/24/solid";
|
||||||
|
|
||||||
|
@ -13,6 +12,7 @@ import { APIClient } from "@api/APIClient";
|
||||||
import { IndexerKeys } from "@api/query_keys";
|
import { IndexerKeys } from "@api/query_keys";
|
||||||
import { IndexersQueryOptions } from "@api/queries";
|
import { IndexersQueryOptions } from "@api/queries";
|
||||||
import { Checkbox } from "@components/Checkbox";
|
import { Checkbox } from "@components/Checkbox";
|
||||||
|
import toast from "@components/hot-toast";
|
||||||
import Toast from "@components/notifications/Toast";
|
import Toast from "@components/notifications/Toast";
|
||||||
import { EmptySimple } from "@components/emptystates";
|
import { EmptySimple } from "@components/emptystates";
|
||||||
import { IndexerAddForm, IndexerUpdateForm } from "@forms";
|
import { IndexerAddForm, IndexerUpdateForm } from "@forms";
|
||||||
|
|
|
@ -7,7 +7,6 @@ import { Fragment, MouseEvent, useEffect, useMemo, useRef, useState } from "reac
|
||||||
import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
|
import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { ArrowPathIcon, LockClosedIcon, LockOpenIcon, PlusIcon } from "@heroicons/react/24/solid";
|
import { ArrowPathIcon, LockClosedIcon, LockOpenIcon, PlusIcon } from "@heroicons/react/24/solid";
|
||||||
import { Menu, MenuButton, MenuItem, MenuItems, Transition } from "@headlessui/react";
|
import { Menu, MenuButton, MenuItem, MenuItems, Transition } from "@headlessui/react";
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
import {
|
import {
|
||||||
ArrowsPointingInIcon,
|
ArrowsPointingInIcon,
|
||||||
ArrowsPointingOutIcon,
|
ArrowsPointingOutIcon,
|
||||||
|
@ -26,6 +25,7 @@ import { IrcKeys } from "@api/query_keys";
|
||||||
import { IrcQueryOptions } from "@api/queries";
|
import { IrcQueryOptions } from "@api/queries";
|
||||||
import { EmptySimple } from "@components/emptystates";
|
import { EmptySimple } from "@components/emptystates";
|
||||||
import { DeleteModal } from "@components/modals";
|
import { DeleteModal } from "@components/modals";
|
||||||
|
import { toast } from "@components/hot-toast";
|
||||||
import Toast from "@components/notifications/Toast";
|
import Toast from "@components/notifications/Toast";
|
||||||
import { SettingsContext } from "@utils/Context";
|
import { SettingsContext } from "@utils/Context";
|
||||||
import { Checkbox } from "@components/Checkbox";
|
import { Checkbox } from "@components/Checkbox";
|
||||||
|
|
|
@ -5,12 +5,12 @@
|
||||||
|
|
||||||
import { useMutation, useSuspenseQuery } from "@tanstack/react-query";
|
import { useMutation, useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { getRouteApi } from "@tanstack/react-router";
|
import { getRouteApi } from "@tanstack/react-router";
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
import Select from "react-select";
|
import Select from "react-select";
|
||||||
|
|
||||||
import { APIClient } from "@api/APIClient";
|
import { APIClient } from "@api/APIClient";
|
||||||
import { ConfigQueryOptions } from "@api/queries";
|
import { ConfigQueryOptions } from "@api/queries";
|
||||||
import { SettingsKeys } from "@api/query_keys";
|
import { SettingsKeys } from "@api/query_keys";
|
||||||
|
import { toast } from "@components/hot-toast";
|
||||||
import Toast from "@components/notifications/Toast";
|
import Toast from "@components/notifications/Toast";
|
||||||
import { LogLevelOptions, SelectOption } from "@domain/constants";
|
import { LogLevelOptions, SelectOption } from "@domain/constants";
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
|
|
||||||
import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
|
import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { PlusIcon } from "@heroicons/react/24/solid";
|
import { PlusIcon } from "@heroicons/react/24/solid";
|
||||||
import toast from "react-hot-toast";
|
|
||||||
|
|
||||||
import { APIClient } from "@api/APIClient";
|
import { APIClient } from "@api/APIClient";
|
||||||
import { NotificationKeys } from "@api/query_keys";
|
import { NotificationKeys } from "@api/query_keys";
|
||||||
|
@ -14,6 +13,7 @@ import { EmptySimple } from "@components/emptystates";
|
||||||
import { useToggle } from "@hooks/hooks";
|
import { useToggle } from "@hooks/hooks";
|
||||||
import { NotificationAddForm, NotificationUpdateForm } from "@forms/settings/NotificationForms";
|
import { NotificationAddForm, NotificationUpdateForm } from "@forms/settings/NotificationForms";
|
||||||
import { componentMapType } from "@forms/settings/DownloadClientForms";
|
import { componentMapType } from "@forms/settings/DownloadClientForms";
|
||||||
|
import toast from "@components/hot-toast";
|
||||||
import Toast from "@components/notifications/Toast";
|
import Toast from "@components/notifications/Toast";
|
||||||
import {
|
import {
|
||||||
DiscordIcon,
|
DiscordIcon,
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
import { useToggle } from "@hooks/hooks.ts";
|
import { useToggle } from "@hooks/hooks.ts";
|
||||||
import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
|
import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { PlusIcon } from "@heroicons/react/24/solid";
|
import { PlusIcon } from "@heroicons/react/24/solid";
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
|
|
||||||
import { APIClient } from "@api/APIClient";
|
import { APIClient } from "@api/APIClient";
|
||||||
import { ProxyKeys } from "@api/query_keys";
|
import { ProxyKeys } from "@api/query_keys";
|
||||||
|
@ -15,6 +14,7 @@ import { Section } from "./_components";
|
||||||
import { EmptySimple } from "@components/emptystates";
|
import { EmptySimple } from "@components/emptystates";
|
||||||
import { Checkbox } from "@components/Checkbox";
|
import { Checkbox } from "@components/Checkbox";
|
||||||
import { ProxyAddForm, ProxyUpdateForm } from "@forms/settings/ProxyForms";
|
import { ProxyAddForm, ProxyUpdateForm } from "@forms/settings/ProxyForms";
|
||||||
|
import { toast } from "@components/hot-toast";
|
||||||
import Toast from "@components/notifications/Toast";
|
import Toast from "@components/notifications/Toast";
|
||||||
|
|
||||||
interface ListItemProps {
|
interface ListItemProps {
|
||||||
|
|
|
@ -5,12 +5,12 @@
|
||||||
|
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { useMutation, useQueryClient, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQueryClient, useQuery } from "@tanstack/react-query";
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
import { MultiSelect as RMSC } from "react-multi-select-component";
|
import { MultiSelect as RMSC } from "react-multi-select-component";
|
||||||
import { AgeSelect } from "@components/inputs"
|
import { AgeSelect } from "@components/inputs"
|
||||||
|
|
||||||
import { APIClient } from "@api/APIClient";
|
import { APIClient } from "@api/APIClient";
|
||||||
import { ReleaseKeys } from "@api/query_keys";
|
import { ReleaseKeys } from "@api/query_keys";
|
||||||
|
import { toast } from "@components/hot-toast";
|
||||||
import Toast from "@components/notifications/Toast";
|
import Toast from "@components/notifications/Toast";
|
||||||
import { useToggle } from "@hooks/hooks";
|
import { useToggle } from "@hooks/hooks";
|
||||||
import { DeleteModal } from "@components/modals";
|
import { DeleteModal } from "@components/modals";
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue