diff --git a/packages/web/package.json b/packages/web/package.json index 4e78e8f..c8a67a4 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -58,5 +58,10 @@ "vite": "^5.2.11", "vite-plugin-graphql-codegen": "^3.3.6", "yup": "^1.4.0" + }, + "dependencies": { + "framer-motion": "^11.2.10", + "immer": "^10.1.1", + "zustand": "^4.5.2" } } diff --git a/packages/web/src/app.tsx b/packages/web/src/app.tsx index 2e94529..01b84c7 100644 --- a/packages/web/src/app.tsx +++ b/packages/web/src/app.tsx @@ -1,17 +1,17 @@ -import type { FC } from 'react'; -import React, { Fragment } from 'react'; -import { Header } from './components/header/header'; -import { Title } from './components/title'; -import './styles/globals.css'; -import { ToastProvider } from './components/toast'; -import { Helmet } from 'react-helmet-async'; +import type { FC } from "react"; +import { Fragment } from "react"; +import { Header } from "./components/header/header"; +import { Title } from "./components/title"; +import "./styles/globals.css"; +import { Helmet } from "react-helmet-async"; +import { ToastProvider } from "./components/toast/toast-provider"; interface AppProps { children: React.ReactNode; } -declare module 'react' { - export type SVGAttributes = import('preact').JSX.SVGAttributes; +declare module "react" { + export type SVGAttributes = import("preact").JSX.SVGAttributes; } export const App: FC = ({ children }) => ( @@ -21,9 +21,8 @@ export const App: FC = ({ children }) => ( - -
-
{children}
- +
+
{children}
+ ); diff --git a/packages/web/src/components/button.tsx b/packages/web/src/components/button.tsx index 35e3bf4..65ec128 100644 --- a/packages/web/src/components/button.tsx +++ b/packages/web/src/components/button.tsx @@ -1,11 +1,11 @@ -import clsx from 'clsx'; -import type { FC, HTMLAttributes } from 'react'; -import { forwardRef } from 'react'; -import { Spinner } from './spinner'; +import clsx from "clsx"; +import type { FC, HTMLAttributes } from "react"; +import { forwardRef } from "react"; +import { Spinner } from "./spinner"; type ButtonBaseProps = Omit< HTMLAttributes, - 'prefix' | 'style' | 'as' | 'loading' + "prefix" | "style" | "as" | "loading" >; export interface ButtonProps extends ButtonBaseProps { @@ -13,23 +13,24 @@ export interface ButtonProps extends ButtonBaseProps { disabled?: boolean; style?: ButtonStyle; loading?: boolean; - type?: 'submit' | 'reset' | 'button'; - as?: FC | 'button' | 'a'; + type?: "submit" | "reset" | "button"; + as?: FC | "button" | "a"; } export enum ButtonStyle { - Primary = 'bg-purple-500 hover:bg-purple-400', - Secondary = 'bg-dark-600 hover:bg-dark-900', - Disabled = 'bg-dark-300 hover:bg-dark-400 cursor-not-allowed', + Primary = "bg-purple-500 hover:bg-purple-400", + Secondary = "bg-dark-600 hover:bg-dark-900", + Disabled = "bg-dark-300 hover:bg-dark-400 cursor-not-allowed", + Transparent = "bg-transparent hover:bg-dark-900", } export const BASE_BUTTON_CLASSES = - 'flex items-center justify-center gap-2 px-3 py-2 text-sm font-medium transition rounded truncate max-h-[2.65em]'; + "flex items-center justify-center gap-2 px-3 py-2 text-sm font-medium transition rounded truncate max-h-[2.65em]"; export const Button = forwardRef( ( { - as: As = 'button', + as: As = "button", disabled, className, type, @@ -54,7 +55,7 @@ export const Button = forwardRef( disabled={disabled} onClick={onClickWrap} onKeyDown={onKeyDownWrap} - style={{ height: '2.5rem' }} + style={{ height: "2.5rem" }} {...rest} ref={ref} > diff --git a/packages/web/src/components/header/header.tsx b/packages/web/src/components/header/header.tsx index 20c6d34..5e48f71 100644 --- a/packages/web/src/components/header/header.tsx +++ b/packages/web/src/components/header/header.tsx @@ -1,8 +1,10 @@ import clsx from "clsx"; import { Fragment, memo, useRef, useState } from "react"; import { FiCrop } from "react-icons/fi"; +import { graphql } from "../../@generated/gql"; import { useAsync } from "../../hooks/useAsync"; import { useConfig } from "../../hooks/useConfig"; +import { useErrorMutation } from "../../hooks/useErrorMutation"; import { useOnClickOutside } from "../../hooks/useOnClickOutside"; import { usePaths } from "../../hooks/usePaths"; import { useUser } from "../../hooks/useUser"; @@ -10,10 +12,9 @@ import { Button, ButtonStyle } from "../button"; import { Container } from "../container"; import { Input } from "../input/input"; import { Link } from "../link"; -import { useToasts } from "../toast"; +import { createToast } from "../toast/store"; +import { ToastStyle } from "../toast/toast"; import { HeaderUser } from "./header-user"; -import { graphql } from "../../@generated/gql"; -import { useErrorMutation } from "../../hooks/useErrorMutation"; const ResendVerificationEmail = graphql(` mutation ResendVerificationEmail($data: ResendVerificationEmailDto) { @@ -28,7 +29,6 @@ export const Header = memo(() => { const [showEmailInput, setShowEmailInput] = useState(false); const emailInputRef = useRef(null); const [email, setEmail] = useState(""); - const createToast = useToasts(); const [resent, setResent] = useState(false); const classes = clsx( "relative z-20 flex items-center justify-between h-16 my-auto transition", @@ -59,8 +59,9 @@ export const Header = memo(() => { } catch (error: any) { if (error.message.includes("You can only") || error.message.includes("You have already")) { createToast({ - text: "You have already requested a verification email. Please check your inbox, or try resend in 5 minutes.", - error: true, + style: ToastStyle.Error, + message: + "You have already requested a verification email. Please check your inbox, or try resend in 5 minutes.", }); return; } diff --git a/packages/web/src/components/syntax-highlighter/syntax-highlighter-controls.tsx b/packages/web/src/components/syntax-highlighter/syntax-highlighter-controls.tsx index 036a3f9..2556dc9 100644 --- a/packages/web/src/components/syntax-highlighter/syntax-highlighter-controls.tsx +++ b/packages/web/src/components/syntax-highlighter/syntax-highlighter-controls.tsx @@ -1,9 +1,9 @@ -import type { Language } from 'prism-react-renderer'; -import { memo } from 'react'; -import { FiChevronDown } from 'react-icons/fi'; -import languages from '../../data/languages.json'; -import { useToasts } from '../toast'; -import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; +import type { Language } from "prism-react-renderer"; +import { memo } from "react"; +import { FiChevronDown } from "react-icons/fi"; +import languages from "../../data/languages.json"; +import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; +import { createToast } from "../toast/store"; export interface SyntaxHighlighterControlsProps { onLanguageChange: (language: Language) => void; @@ -13,10 +13,9 @@ export interface SyntaxHighlighterControlsProps { export const SyntaxHighlighterControls = memo( ({ language, content, onLanguageChange }) => { - const createToast = useToasts(); const copyContent = () => { navigator.clipboard.writeText(content); - createToast({ text: 'Copied file content to clipboard.' }); + createToast({ message: "Copied file content to clipboard." }); }; return ( @@ -25,7 +24,7 @@ export const SyntaxHighlighterControls = memo( {language} - + {languages.map((language) => ( void); -export const ToastContext = React.createContext(null); diff --git a/packages/web/src/components/toast/index.ts b/packages/web/src/components/toast/index.ts deleted file mode 100644 index 968b5ab..0000000 --- a/packages/web/src/components/toast/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./context"; -export * from "./toast-provider"; -export * from "./useToasts"; diff --git a/packages/web/src/components/toast/store.ts b/packages/web/src/components/toast/store.ts new file mode 100644 index 0000000..39e85ab --- /dev/null +++ b/packages/web/src/components/toast/store.ts @@ -0,0 +1,104 @@ +import type { ReactNode } from "react"; +import { create } from "zustand"; +import { immer } from "zustand/middleware/immer"; +import { ToastStyle, type ToastProps } from "./toast"; + +const TRANSITION_INTERVAL = 150; +let currentToastId = 0; + +export interface ToastOptions { + id: number; + message: ReactNode; + className?: string; + removing?: boolean; + style?: ToastStyle; + duration?: number | null; + onConfirm?: () => void; + onCancel?: () => void; + onClose?: () => void; +} + +type State = { + items: ToastProps[]; +}; + +const useToastStore = create()( + immer((set) => ({ + items: [], + })), +); + +export const removeToast = (id: number) => { + let hasToast: boolean | undefined; + useToastStore.setState((draft) => { + const toast = draft.items.find((toast) => toast.id === id); + if (toast) { + if (toast.removing) return; + toast.removing = true; + hasToast = true; + } + }); + + if (hasToast) { + setTimeout(() => { + useToastStore.setState((draft) => { + draft.items = draft.items.filter((item) => item.id !== id); + }); + }, TRANSITION_INTERVAL); + } +}; + +export const createToast = (options: Omit) => { + if (typeof window === "undefined") return; + const id = currentToastId++; + const hasButtons = !!(options.onConfirm || options.onCancel); + const timer: NodeJS.Timeout | undefined = undefined; + const onRemove = () => { + if (timer) clearTimeout(timer); + removeToast(id); + if (options.onClose) { + options.onClose(); + } + }; + + const duration = + options.duration === null ? options.duration : options.duration || (hasButtons ? 10000 : 5000); + const expiresAt = duration === null ? null : Date.now() + duration; + const toast: ToastProps = { + ...options, + id: id, + removing: false, + expiresAt: expiresAt, + createdAt: Date.now(), + onRemove, + }; + + useToastStore.setState((draft) => { + const existingToast = draft.items.find((toast) => toast.message === options.message); + if (existingToast) { + console.debug( + `Toast with message "${options.message}" already exists, refreshing time on existing toast.`, + ); + existingToast.expiresAt = expiresAt; + existingToast.createdAt = Date.now(); + return; + } + + draft.items.push(toast); + }); + + return toast; +}; + +export const updateToast = (id: number, options: Partial) => { + useToastStore.setState((draft) => { + const toast = draft.items.find((toast) => toast.id === id); + if (toast) { + Object.assign(toast, options); + } + }); +}; + +export const useToasts = () => { + return useToastStore((store) => store.items); +}; diff --git a/packages/web/src/components/toast/toast-provider.tsx b/packages/web/src/components/toast/toast-provider.tsx index d73e761..8a8c8ca 100644 --- a/packages/web/src/components/toast/toast-provider.tsx +++ b/packages/web/src/components/toast/toast-provider.tsx @@ -1,59 +1,20 @@ -import { nanoid } from 'nanoid'; -import type { FC, ReactNode } from 'react'; -import { useCallback, useState } from 'preact/hooks'; -import { Fragment } from 'preact'; -import { ToastContext } from './context'; -import type { ToastProps } from './toast'; -import { TRANSITION_DURATION, Toast } from './toast'; +import { memo, useState } from "react"; +import { useToasts } from "./store"; +import { Toast } from "./toast"; -// spread operators on arrays are to fix this -// https://stackoverflow.com/questions/56266575/why-is-usestate-not-triggering-re-render - -export const ToastProvider: FC<{ children: ReactNode }> = (props) => { - const [toasts, setToasts] = useState<(ToastProps & { id: string; timer: NodeJS.Timeout })[]>([]); - const createToast = useCallback( - (toast: ToastProps) => { - if (toasts.some((existing) => existing.text === toast.text)) { - // skip duplicate cards - return; - } - - const timeout = toast.timeout ?? 5000; - const id = nanoid(); - const timer = setTimeout(() => { - // set removing on toast starting the transition - setToasts((toasts) => { - for (const toast of toasts) { - if (toast.id !== id) continue; - toast.removing = true; - } - - return [...toasts]; - }); - - setTimeout(() => { - // remove toast once transition is complete - setToasts((toasts) => toasts.filter((toast) => toast.id !== id)); - }, TRANSITION_DURATION); - }, timeout - TRANSITION_DURATION); - - // create toast - setToasts((toasts) => { - const data = Object.assign(toast, { id, timer }); - return [...toasts, data]; - }); - }, - [toasts, setToasts], - ); +export const ToastProvider = memo(() => { + const toasts = useToasts(); + const [hovered, setHovered] = useState(false); return ( - - {props.children} -
- {toasts.map((toast) => ( - - ))} -
-
+
setHovered(true)} + onMouseLeave={() => setHovered(false)} + > + {toasts.map((toast) => ( + + ))} +
); -}; +}); diff --git a/packages/web/src/components/toast/toast.tsx b/packages/web/src/components/toast/toast.tsx index b89149f..5e06e56 100644 --- a/packages/web/src/components/toast/toast.tsx +++ b/packages/web/src/components/toast/toast.tsx @@ -1,35 +1,152 @@ -import clsx from 'clsx'; -import type { FC } from 'react'; -import { useEffect, useState } from 'react'; +import { clsx } from "clsx"; +import { memo, type ReactNode, useEffect, useState } from "react"; +import { FiInfo, FiX } from "react-icons/fi"; +import { Button, ButtonStyle } from "../button"; +import { removeToast, updateToast } from "./store"; +import { motion } from "framer-motion"; export interface ToastProps { - text: string; - error?: boolean; - timeout?: number; - removing?: boolean; + id: number; + message: ReactNode; + className?: string; + removing: boolean; + expiresAt: number | null; + createdAt: number; + paused?: boolean; + style?: ToastStyle; + onConfirm?: () => void; + onCancel?: () => void; + onRemove: () => void; } -export const TRANSITION_DURATION = 300; -export const Toast: FC = (props) => { - const initialClasses = 'opacity-0 scale-90'; - const animateClasses = 'opacity-100 translate-x-0'; - const [transition, setTransition] = useState(initialClasses); - const classes = clsx('p-4 transition duration-300 rounded shadow-xl select-none w-96', transition, { - 'bg-violet-500': !props.error, - 'bg-red-600': props.error, - }); - - useEffect(() => { - if (props.removing) setTransition(initialClasses); - else { - // breaks the browser trying to optimise the transition by skipping it because we add it so fast - requestAnimationFrame(() => { - setTimeout(() => { - setTransition(animateClasses); - }); - }); - } - }, [props.removing]); - - return
{props.text}
; -}; +export enum ToastStyle { + Default = 0, + Error = 1, +} + +export const Toast = memo( + ({ + id, + removing, + message, + paused, + expiresAt, + createdAt, + style = ToastStyle.Default, + onRemove, + onCancel: onCancelHandler, + onConfirm: onConfirmHandler, + }) => { + const hasButtons = !!(onCancelHandler || onConfirmHandler); + const [expiryProgress, setExpiryProgress] = useState(null); + const [pausedWith, setPausedWith] = useState(null); + const duration = expiresAt ? expiresAt - createdAt : null; + const classes = clsx( + "p-2 rounded flex gap-2 relative text-sm items-center overflow-hidden md:max-w-[40em]", + style === ToastStyle.Default && "bg-purple-600", + style === ToastStyle.Error && "bg-red-600", + ); + const progressClasses = clsx( + "absolute bottom-0 left-0 h-[2px] bg-brand-pink-400", + style === ToastStyle.Default && "bg-purple-400", + style === ToastStyle.Error && "bg-red-400", + ); + + const onCancel = () => { + onRemove(); + onCancelHandler && onCancelHandler(); + }; + + const onConfirm = () => { + onRemove(); + onConfirmHandler && onConfirmHandler(); + }; + + useEffect(() => { + if (!expiresAt) return; + if (paused && !pausedWith) { + // when paused becomes true, we store the time we paused at + // so we can resume later on + const pausedWith = expiresAt - Date.now(); + setPausedWith(pausedWith); + } else if (pausedWith) { + // when paused changes to false, we reset the expiry and createdAt + // to "resume" the expiry countdown + setPausedWith(null); + if (duration) { + const newExpiresAt = Date.now() + pausedWith; + updateToast(id, { + expiresAt: newExpiresAt, + createdAt: newExpiresAt - duration, + }); + } + } + }, [paused]); + + useEffect(() => { + if (expiresAt && !pausedWith) { + const interval = setInterval(() => { + const expiresIn = expiresAt - Date.now(); + const progress = 1 - expiresIn / (expiresAt - createdAt); + setExpiryProgress(progress); + if (progress >= 1) { + removeToast(id); + clearInterval(interval); + } + }, 3); + + return () => clearInterval(interval); + } + }, [pausedWith]); + + return ( + // + + +
+ + {!hasButtons && ( + + )} +
{message}
+
+ {hasButtons && ( + + )} + {onConfirmHandler && ( + + )} +
+ {!!expiryProgress && ( +
+ )} +
+ + ); + }, +); diff --git a/packages/web/src/components/toast/useToasts.tsx b/packages/web/src/components/toast/useToasts.tsx deleted file mode 100644 index 86b92a0..0000000 --- a/packages/web/src/components/toast/useToasts.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { useContext } from 'react'; -import { ToastContext } from './context'; - -export const useToasts = () => { - const createToast = useContext(ToastContext); - if (!createToast) { - if (typeof window === 'undefined') return () => {}; - throw new Error('useToasts must be used within a ToastProvider'); - } - - return createToast; -}; diff --git a/packages/web/src/hooks/useErrorMutation.ts b/packages/web/src/hooks/useErrorMutation.ts index a668b6f..eaac450 100644 --- a/packages/web/src/hooks/useErrorMutation.ts +++ b/packages/web/src/hooks/useErrorMutation.ts @@ -7,14 +7,14 @@ import { } from "@urql/preact"; import { useCallback } from "preact/hooks"; import { getErrorMessage } from "../helpers/get-error-message.helper"; -import { useToasts } from "../components/toast"; +import { createToast } from "../components/toast/store"; +import { ToastStyle } from "../components/toast/toast"; export const useErrorMutation = ( query: DocumentInput, handleErrors?: boolean, ): UseMutationResponse => { const [data, mutation] = useMutation(query); - const createToast = useToasts(); const wrappedMutation = useCallback( async (variables: Variables, context?: Partial | undefined) => { const result = await mutation(variables, context); @@ -22,7 +22,7 @@ export const useErrorMutation = { const config = useConfig(); @@ -13,8 +13,8 @@ export const Page: FC = () => {

Micro

- An invite-only file sharing and paste service with vanity domains and a ShareX compatible endpoint. Sign in to - download a generated ShareX configuration. You can view the source code{' '} + An invite-only file sharing and paste service with vanity domains and a ShareX compatible endpoint. + Sign in to download a generated ShareX configuration. You can view the source code{" "} here. @@ -27,7 +27,7 @@ export const Page: FC = () => { {!config.data && } {config.data && (

- To get an account or get a file taken down, email{' '} + To get an account or get a file taken down, email{" "} {config.data.inquiriesEmail} diff --git a/packages/web/src/pages/dashboard/mfa/+Page.tsx b/packages/web/src/pages/dashboard/mfa/+Page.tsx index a9a62f3..92034e5 100644 --- a/packages/web/src/pages/dashboard/mfa/+Page.tsx +++ b/packages/web/src/pages/dashboard/mfa/+Page.tsx @@ -11,11 +11,11 @@ import { Error } from "../../../components/error"; import { OtpInput } from "../../../components/input/otp"; import { PageLoader } from "../../../components/page-loader"; import { Steps } from "../../../components/steps"; -import { useToasts } from "../../../components/toast"; import { navigate } from "../../../helpers/routing"; import { useAsync } from "../../../hooks/useAsync"; import { useQueryState } from "../../../hooks/useQueryState"; import { useErrorMutation } from "../../../hooks/useErrorMutation"; +import { createToast } from "../../../components/toast/store"; const GenerateOtp = graphql(` query GenerateOTP { @@ -35,7 +35,6 @@ const ConfirmOTP = graphql(` export const Page: FC = () => { const [result] = useQuery({ query: GenerateOtp }); - const createToast = useToasts(); const [currentStep, setCurrentStep] = useQueryState("step", 0, Number); const [, confirmOtp] = useErrorMutation(ConfirmOTP); @@ -61,13 +60,13 @@ export const Page: FC = () => { if (!copyable) return; navigator.clipboard.writeText(copyable); createToast({ - text: "Copied recovery codes!", + message: "Copied recovery codes!", }); }, [createToast, copyable]); const [confirm, confirming] = useAsync(async (otpCode: string) => { await confirmOtp({ otpCode }); - createToast({ text: "Successfully enabled 2FA!" }); + createToast({ message: "Successfully enabled 2FA!" }); navigate("/dashboard", { overwriteLastHistoryEntry: true }); }); diff --git a/packages/web/src/pages/dashboard/preferences/change-password/+Page.tsx b/packages/web/src/pages/dashboard/preferences/change-password/+Page.tsx index edaf660..a377847 100644 --- a/packages/web/src/pages/dashboard/preferences/change-password/+Page.tsx +++ b/packages/web/src/pages/dashboard/preferences/change-password/+Page.tsx @@ -4,12 +4,12 @@ import type { ChangePasswordMutationVariables } from "../../../../@generated/gra import { Breadcrumbs } from "../../../../components/breadcrumbs"; import { Container } from "../../../../components/container"; import { Title } from "../../../../components/title"; -import { useToasts } from "../../../../components/toast"; import { PasswordForm } from "../../../../containers/password-form"; import { navigate, prefetch } from "../../../../helpers/routing"; import { useAsync } from "../../../../hooks/useAsync"; import { useErrorMutation } from "../../../../hooks/useErrorMutation"; import { useUser } from "../../../../hooks/useUser"; +import { createToast } from "../../../../components/toast/store"; const ChangePassword = graphql(` mutation ChangePassword($oldPassword: String!, $newPassword: String!) { @@ -18,20 +18,19 @@ const ChangePassword = graphql(` `); export const Page: FC = () => { - const createToast = useToasts(); const [, changeInner] = useErrorMutation(ChangePassword, false); const [change] = useAsync(async (values: ChangePasswordMutationVariables) => { prefetch("/dashboard/preferences"); try { await changeInner(values); - createToast({ text: "Your password has been changed." }); + createToast({ message: "Your password has been changed." }); navigate("/dashboard/preferences"); } catch (error: any) { if (error.message.toLowerCase().includes("unauthorized")) { - createToast({ text: "Invalid password." }); + createToast({ message: "Invalid password." }); return; } else { - createToast({ text: "An error occurred changing your password." }); + createToast({ message: "An error occurred changing your password." }); } } }); diff --git a/packages/web/src/pages/file/@fileId/+Page.tsx b/packages/web/src/pages/file/@fileId/+Page.tsx index d46a4b0..f2aee42 100644 --- a/packages/web/src/pages/file/@fileId/+Page.tsx +++ b/packages/web/src/pages/file/@fileId/+Page.tsx @@ -11,13 +11,13 @@ import { Error } from "../../../components/error"; import { Skeleton, SkeletonList } from "../../../components/skeleton"; import { Spinner } from "../../../components/spinner"; import { Title } from "../../../components/title"; -import { useToasts } from "../../../components/toast"; import { downloadUrl } from "../../../helpers/download.helper"; import { navigate } from "../../../helpers/routing"; import { useAsync } from "../../../hooks/useAsync"; import { useQueryState } from "../../../hooks/useQueryState"; import type { PageProps } from "../../../renderer/types"; import { useErrorMutation } from "../../../hooks/useErrorMutation"; +import { createToast } from "../../../components/toast/store"; const GetFile = graphql(` query GetFile($fileId: ID!) { @@ -72,7 +72,6 @@ export const Page: FC = ({ routeParams }) => { const fileId = routeParams.fileId; const [deleteKey] = useQueryState("deleteKey"); const [confirm, setConfirm] = useState(false); - const createToast = useToasts(); const [file] = useQuery({ query: GetFile, pause: !fileId, @@ -85,7 +84,7 @@ export const Page: FC = ({ routeParams }) => { const copyLink = () => { copyToClipboard(file.data?.file.urls.view ?? window.location.href); createToast({ - text: "Copied link to clipboard", + message: "Copied link to clipboard", }); }; @@ -106,7 +105,7 @@ export const Page: FC = ({ routeParams }) => { deleteKey: deleteKey, }); - createToast({ text: `Deleted "${file.data.file.displayName}"` }); + createToast({ message: `Deleted "${file.data.file.displayName}"` }); navigate("/dashboard", { overwriteLastHistoryEntry: true }); }); diff --git a/packages/web/src/pages/invite/@inviteToken/+Page.tsx b/packages/web/src/pages/invite/@inviteToken/+Page.tsx index fa427ee..f1a529a 100644 --- a/packages/web/src/pages/invite/@inviteToken/+Page.tsx +++ b/packages/web/src/pages/invite/@inviteToken/+Page.tsx @@ -6,7 +6,6 @@ import { Error } from "../../../components/error"; import { PageLoader } from "../../../components/page-loader"; import { Time } from "../../../components/time"; import { Title } from "../../../components/title"; -import { useToasts } from "../../../components/toast"; import type { SignupData } from "../../../containers/signup-form"; import { SignupForm } from "../../../containers/signup-form"; import { navigate, prefetch } from "../../../helpers/routing"; @@ -15,6 +14,7 @@ import { useConfig } from "../../../hooks/useConfig"; import type { PageProps } from "../../../renderer/types"; import { useQuery } from "@urql/preact"; import { useErrorMutation } from "../../../hooks/useErrorMutation"; +import { createToast } from "../../../components/toast/store"; const GetInvite = graphql(` query GetInvite($inviteId: ID!) { @@ -35,7 +35,6 @@ const CreateUser = graphql(` export const Page: FC = ({ routeParams }) => { const config = useConfig(); - const createToast = useToasts(); const inviteToken = routeParams.inviteToken; const [invite] = useQuery({ query: GetInvite, pause: !inviteToken, variables: { inviteId: inviteToken! } }); const expiresAt = invite.data?.invite.expiresAt; @@ -55,7 +54,7 @@ export const Page: FC = ({ routeParams }) => { }); navigate("/login"); - createToast({ text: "Account created successfully. Please sign in." }); + createToast({ message: "Account created successfully. Please sign in." }); }); if (invite.error || config.error) { diff --git a/packages/web/src/pages/login/+Page.tsx b/packages/web/src/pages/login/+Page.tsx index 8a25fb3..a522aa1 100644 --- a/packages/web/src/pages/login/+Page.tsx +++ b/packages/web/src/pages/login/+Page.tsx @@ -1,21 +1,19 @@ -import type { FC } from 'react'; -import { useEffect } from 'react'; -import { Title } from '../../components/title'; -import { LoginForm } from '../../containers/login-form'; -import { Container } from '../../components/container'; -import { useToasts } from '../../components/toast'; +import type { FC } from "react"; +import { useEffect } from "react"; +import { Title } from "../../components/title"; +import { LoginForm } from "../../containers/login-form"; +import { Container } from "../../components/container"; +import { createToast } from "../../components/toast/store"; export const Page: FC = () => { - const createToast = useToasts(); - useEffect(() => { // show a verification toast if the user has // completed verification const url = new URL(window.location.href); - const verified = url.searchParams.get('verified'); + const verified = url.searchParams.get("verified"); if (verified) { createToast({ - text: 'Your account has been verified.', + message: "Your account has been verified.", }); } }, [createToast]); diff --git a/packages/web/src/pages/upload/+Page.tsx b/packages/web/src/pages/upload/+Page.tsx index c14ea9e..9724135 100644 --- a/packages/web/src/pages/upload/+Page.tsx +++ b/packages/web/src/pages/upload/+Page.tsx @@ -1,20 +1,21 @@ -import type { ChangeEventHandler, FC, JSX } from 'react'; -import { useEffect, useRef, useState } from 'react'; -import { FiUpload } from 'react-icons/fi'; -import { Button } from '../../components/button'; -import { Card } from '../../components/card'; -import { Container } from '../../components/container'; -import { Select } from '../../components/input/select'; -import { PageLoader } from '../../components/page-loader'; -import { Spinner } from '../../components/spinner'; -import { Title } from '../../components/title'; -import { useToasts } from '../../components/toast'; -import { getErrorMessage } from '../../helpers/get-error-message.helper'; -import { http } from '../../helpers/http.helper'; -import { replaceUsername } from '../../helpers/replace-username.helper'; -import { navigate } from '../../helpers/routing'; -import { useConfig } from '../../hooks/useConfig'; -import { useUser } from '../../hooks/useUser'; +import type { ChangeEventHandler, FC, JSX } from "react"; +import { useEffect, useRef, useState } from "react"; +import { FiUpload } from "react-icons/fi"; +import { Button } from "../../components/button"; +import { Card } from "../../components/card"; +import { Container } from "../../components/container"; +import { Select } from "../../components/input/select"; +import { PageLoader } from "../../components/page-loader"; +import { Spinner } from "../../components/spinner"; +import { Title } from "../../components/title"; +import { getErrorMessage } from "../../helpers/get-error-message.helper"; +import { http } from "../../helpers/http.helper"; +import { replaceUsername } from "../../helpers/replace-username.helper"; +import { navigate } from "../../helpers/routing"; +import { useConfig } from "../../hooks/useConfig"; +import { useUser } from "../../hooks/useUser"; +import { createToast } from "../../components/toast/store"; +import { ToastStyle } from "../../components/toast/toast"; interface CreateFileResponse { id: string; @@ -30,7 +31,6 @@ export const Page: FC = () => { const [uploading, setUploading] = useState(false); const [file, setFile] = useState(null); const [hover, setHover] = useState(false); - const createToast = useToasts(); const [selectedHost, setSelectedHost] = useState(); const config = useConfig(); @@ -61,9 +61,9 @@ export const Page: FC = () => { } }; - document.addEventListener('paste', onPaste); + document.addEventListener("paste", onPaste); return () => { - document.removeEventListener('paste', onPaste); + document.removeEventListener("paste", onPaste); }; }, []); @@ -83,9 +83,9 @@ export const Page: FC = () => { const form = new FormData(); form.append(file.name, file); const headers: HeadersInit = {}; - if (selectedHost) headers['X-Micro-Host'] = selectedHost; - const response = await http(`file`, { - method: 'POST', + if (selectedHost) headers["X-Micro-Host"] = selectedHost; + const response = await http("file", { + method: "POST", body: form, headers: headers, }); @@ -100,8 +100,8 @@ export const Page: FC = () => { location.href = body.urls.view; setFile(null); } catch (error: unknown) { - const message = getErrorMessage(error) ?? 'An unknown error occured.'; - createToast({ error: true, text: message }); + const message = getErrorMessage(error) ?? "An unknown error occured."; + createToast({ style: ToastStyle.Error, message: message }); } finally { setUploading(false); } @@ -142,7 +142,11 @@ export const Page: FC = () => { onChange={(event) => setSelectedHost(event.currentTarget.value)} > {config.data.hosts.map((host) => ( - ))} @@ -150,6 +154,7 @@ export const Page: FC = () => {