From b0809c65f3a94d5a7d87f292c3dff17be2f4c1cf Mon Sep 17 00:00:00 2001 From: Juan Di Toro Date: Tue, 12 Mar 2024 13:42:17 +0100 Subject: [PATCH 1/3] feat: (wip) added support for TUS file uploaded. Still missing support for resetting the uppy state plus handling uppy errors --- app/components/dashboard-layout.tsx | 307 ++++++++++++++- app/components/lib/uppy-dropzone.ts | 206 ++++++++++ app/components/lib/uppy.ts | 159 ++++++++ app/components/ui/button.tsx | 2 +- app/components/ui/dialog.tsx | 120 ++++++ app/components/ui/progress.tsx | 26 ++ app/routes/dashboard.tsx | 1 + app/tailwind.css | 1 + package-lock.json | 592 +++++++++++++++++++++++++++- package.json | 5 + tailwind.config.ts | 7 +- vite.config.ts | 21 +- 12 files changed, 1425 insertions(+), 22 deletions(-) create mode 100644 app/components/lib/uppy-dropzone.ts create mode 100644 app/components/lib/uppy.ts create mode 100644 app/components/ui/dialog.tsx create mode 100644 app/components/ui/progress.tsx diff --git a/app/components/dashboard-layout.tsx b/app/components/dashboard-layout.tsx index dcc8c60..422d765 100644 --- a/app/components/dashboard-layout.tsx +++ b/app/components/dashboard-layout.tsx @@ -3,6 +3,17 @@ import logoPng from "~/images/lume-logo.png?url" import lumeColorLogoPng from "~/images/lume-color-logo.png?url" import discordLogoPng from "~/images/discord-logo.png?url" import { Link } from "@remix-run/react" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger +} from "./ui/dialog" +import { useUppy } from "./lib/uppy" +import { UppyFile } from "@uppy/core" +import { Progress } from "./ui/progress" +import { DialogClose } from "@radix-ui/react-dialog" export const DashboardLayout = ({ children }: React.PropsWithChildren<{}>) => { return ( @@ -39,14 +50,11 @@ export const DashboardLayout = ({ children }: React.PropsWithChildren<{}>) => { -

Freedom

-

Privacy

-

Ownership

+

Freedom

+

Privacy

+

Ownership

- + {children} @@ -81,9 +89,152 @@ export const DashboardLayout = ({ children }: React.PropsWithChildren<{}>) => { ) } +const UploadFileModal = () => { + const { getRootProps, getInputProps, files, upload, removeFile, cancelAll } = + useUppy({ + uploader: "tus", + endpoint: import.meta.env.VITE_PUBLIC_TUS_ENDPOINT + }) + + const isUploading = + files.length > 0 + ? files.map((file) => file.progress?.uploadStarted != null).includes(true) + : false + const isCompleted = + files.length > 0 + ? files + .map((file) => file.progress?.uploadComplete) + .reduce((acc, cur) => acc && cur, true) + : false + const hasStarted = isUploading || isCompleted + + return ( + + + + + + + Upload Files + + {!hasStarted ? ( +
+ + +

Drag & Drop Files or Browse

+
+ ) : null} + +
+ {files.map((file) => ( + { + removeFile(id) + }} + /> + ))} +
+ + {hasStarted ? ( +
+ + {isCompleted + ? "Upload completed" + : `${files.length} being uploaded`} +
+ ) : null} + + {hasStarted ? ( + + + + ) : ( + + )} + + {hasStarted && isCompleted ? ( + + + + ) : null} +
+
+ ) +} + +function bytestoMegabytes(bytes: number) { + return bytes / 1024 / 1024 +} + +const UploadFileItem = ({ + file, + onRemove +}: { + file: UppyFile + onRemove: (id: string) => void +}) => { + return ( +
+
+
+
+ {file.progress?.uploadComplete ? ( + + ) : ( + + )} +
+

+ + {file.name} + {" "} + ({bytestoMegabytes(file.size).toFixed(2)} MB) +

+
+ +
+ + {file.progress?.uploadStarted && !file.progress.uploadComplete ? ( + + ) : null} +
+ ) +} + const NavigationButton = ({ children }: React.PropsWithChildren) => { return ( - ) @@ -164,3 +315,143 @@ const CloudUploadIcon = ({ className }: { className?: string }) => { ) } + +const PageIcon = ({ className }: { className?: string }) => { + return ( + + ) +} + +const TrashIcon = ({ className }: { className?: string }) => { + return ( + + ) +} + +const CloudCheckIcon = ({ className }: { className?: string }) => { + return ( + + ) +} + +const BoxCheckedIcon = ({ className }: { className?: string }) => { + return ( + + ) +} diff --git a/app/components/lib/uppy-dropzone.ts b/app/components/lib/uppy-dropzone.ts new file mode 100644 index 0000000..ea46d50 --- /dev/null +++ b/app/components/lib/uppy-dropzone.ts @@ -0,0 +1,206 @@ +// Copied from https://github.com/transloadit/uppy/blob/main/packages/%40uppy/drop-target/src/index.ts +// Is a less invasive implementation that allows for better unstyled integration + +import { + type Uppy, + type PluginOptions, + type BasePlugin as TUppyBasePlugin +} from "@uppy/core" +// @ts-expect-error -- Uppy types are all over the place it really is weird +import UppyBasePlugin from "@uppy/core/lib/BasePlugin" +import type { IndexedObject } from "@uppy/utils" +import getDroppedFiles from "@uppy/utils/lib/getDroppedFiles" +import toArray from "@uppy/utils/lib/toArray" + +export type DropTargetOptions = PluginOptions & { + target?: HTMLElement | string | null + onDrop?: (event: DragEvent) => void + onDragOver?: (event: DragEvent) => void + onDragLeave?: (event: DragEvent) => void +} +const BasePlugin = UppyBasePlugin as typeof TUppyBasePlugin + +type Meta = { + relativePath?: string | null +} +type Body = IndexedObject + +// Default options +const defaultOpts = { + target: null, +} satisfies DropTargetOptions + +interface DragEventWithFileTransfer extends DragEvent { + dataTransfer: NonNullable +} + +function isFileTransfer(event: DragEvent): event is DragEventWithFileTransfer { + return event.dataTransfer?.types?.some((type) => type === "Files") ?? false +} + +/** + * Drop Target plugin + * + */ +class DropTarget extends BasePlugin { + static VERSION = "lume-internal" + + private removeDragOverDataAttr: ReturnType | undefined + private nodes?: Array + + public opts: DropTargetOptions + + constructor(uppy: Uppy, opts?: DropTargetOptions) { + super(uppy, { ...defaultOpts, ...opts }) + this.opts = opts || defaultOpts + this.type = "acquirer" + this.id = this.opts.id || "DropTarget" + // @ts-expect-error TODO: remove in major + this.title = "Drop Target" + } + + addFiles = (files: Array): void => { + const descriptors = files.map((file) => ({ + source: this.id, + name: file.name, + type: file.type, + data: file, + meta: { + // path of the file relative to the ancestor directory the user selected. + // e.g. 'docs/Old Prague/airbnb.pdf' + relativePath: (file as { relativePath?: string }).relativePath || null + } as Meta + })) + + try { + this.uppy.addFiles(descriptors) + } catch (err) { + this.uppy.log(err as string) + } + } + + handleDrop = async (event: DragEvent): Promise => { + if (!isFileTransfer(event)) { + return + } + + event.preventDefault() + event.stopPropagation() + clearTimeout(this.removeDragOverDataAttr) + + // Remove dragover class + if (event.currentTarget) { + (event.currentTarget as HTMLElement).dataset.uppyIsDragOver = "false" + this.setPluginState({ isDraggingOver: false }) + } + + // Let any acquirer plugin (Url/Webcam/etc.) handle drops to the root + this.uppy.iteratePlugins((plugin) => { + if (plugin.type === "acquirer") { + // @ts-expect-error Every Plugin with .type acquirer can define handleRootDrop(event) + plugin.handleRootDrop?.(event) + } + }) + + // Add all dropped files, handle errors + let executedDropErrorOnce = false + const logDropError = (error: Error): void => { + this.uppy.log(error.message, "error") + + // In practice all drop errors are most likely the same, + // so let's just show one to avoid overwhelming the user + if (!executedDropErrorOnce) { + this.uppy.info(error.message, "error") + executedDropErrorOnce = true + } + } + + const files = await getDroppedFiles(event.dataTransfer, { logDropError }) + if (files.length > 0) { + this.uppy.log("[DropTarget] Files were dropped") + this.addFiles(files) + } + + this.opts.onDrop?.(event) + } + + handleDragOver = (event: DragEvent): void => { + if (!isFileTransfer(event)) { + return + } + + event.preventDefault() + event.stopPropagation() + + // Add a small (+) icon on drop + // (and prevent browsers from interpreting this as files being _moved_ into the browser, + // https://github.com/transloadit/uppy/issues/1978) + event.dataTransfer.dropEffect = "copy" // eslint-disable-line no-param-reassign + + clearTimeout(this.removeDragOverDataAttr) + ;(event.currentTarget as HTMLElement).dataset.uppyIsDragOver = "true" + this.setPluginState({ isDraggingOver: true }) + this.opts.onDragOver?.(event) + } + + handleDragLeave = (event: DragEvent): void => { + if (!isFileTransfer(event)) { + return + } + + event.preventDefault() + event.stopPropagation() + + const { currentTarget } = event + + clearTimeout(this.removeDragOverDataAttr) + // Timeout against flickering, this solution is taken from drag-drop library. + // Solution with 'pointer-events: none' didn't work across browsers. + this.removeDragOverDataAttr = setTimeout(() => { + (currentTarget as HTMLElement).dataset.uppyIsDragOver = "false" + this.setPluginState({ isDraggingOver: false }) + }, 50) + this.opts.onDragLeave?.(event) + } + + addListeners = (): void => { + const { target } = this.opts + + if (target instanceof Element) { + this.nodes = [target] + } else if (typeof target === "string") { + this.nodes = toArray(document.querySelectorAll(target)) + } + + if (!this.nodes || this.nodes.length === 0) { + throw new Error(`"${target}" does not match any HTML elements`) + } + + for (const node of this.nodes) { + node.addEventListener("dragover", this.handleDragOver, false) + node.addEventListener("dragleave", this.handleDragLeave, false) + node.addEventListener("drop", this.handleDrop, false) + } + } + + removeListeners = (): void => { + if (this.nodes) { + for (const node of this.nodes) { + node.removeEventListener("dragover", this.handleDragOver, false) + node.removeEventListener("dragleave", this.handleDragLeave, false) + node.removeEventListener("drop", this.handleDrop, false) + } + } + } + + install(): void { + this.setPluginState({ isDraggingOver: false }) + this.addListeners() + } + + uninstall(): void { + this.removeListeners() + } +} + +export default DropTarget diff --git a/app/components/lib/uppy.ts b/app/components/lib/uppy.ts new file mode 100644 index 0000000..807b695 --- /dev/null +++ b/app/components/lib/uppy.ts @@ -0,0 +1,159 @@ +import Uppy, { State, debugLogger } from "@uppy/core" + +import Tus from "@uppy/tus" +import toArray from "@uppy/utils/lib/toArray" + +import { + ChangeEvent, + useCallback, + useEffect, + useMemo, + useRef, + useState +} from "react" +import DropTarget, { DropTargetOptions } from "./uppy-dropzone" + +const LISTENING_EVENTS = [ + "upload", + "upload-success", + "upload-error", + "file-added", + "file-removed", + "files-added" +] as const + +export function useUppy({ + uploader, + endpoint +}: { + uploader: "tus" + endpoint: string +}) { + const inputRef = useRef(null) + const [targetRef, _setTargetRef] = useState(null) + const uppyInstance = useRef() + const setRef = useCallback( + (element: HTMLElement | null) => _setTargetRef(element), + [] + ) + const [state, setState] = useState() + + const [inputProps, setInputProps] = useState< + | { + ref: typeof inputRef + type: "file" + onChange: (event: ChangeEvent) => void + } + | object + >({}) + const getRootProps = useMemo( + () => () => { + return { + ref: setRef, + onClick: () => { + if (inputRef.current) { + //@ts-expect-error -- dumb html + inputRef.current.value = null + inputRef.current.click() + console.log("clicked", { input: inputRef.current }) + } + }, + role: "presentation" + } + }, + [setRef] + ) + const removeFile = useCallback( + (id: string) => { + uppyInstance.current?.removeFile(id) + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [targetRef, uppyInstance] + ) + const cancelAll = useCallback( + () => uppyInstance.current?.cancelAll(), + // eslint-disable-next-line react-hooks/exhaustive-deps + [targetRef, uppyInstance] + ) + + useEffect(() => { + if (!targetRef) return + + const uppy = new Uppy({ logger: debugLogger }).use(DropTarget, { + target: targetRef + } as DropTargetOptions) + + uppyInstance.current = uppy + setInputProps({ + ref: inputRef, + type: "file", + onChange: (event) => { + const files = toArray(event.target.files) + if (files.length > 0) { + uppyInstance.current?.log("[DragDrop] Files selected through input") + uppyInstance.current?.addFiles(files) + } + + // We clear the input after a file is selected, because otherwise + // change event is not fired in Chrome and Safari when a file + // with the same name is selected. + // ___Why not use value="" on instead? + // Because if we use that method of clearing the input, + // Chrome will not trigger change if we drop the same file twice (Issue #768). + // @ts-expect-error TS freaks out, but this is fine + // eslint-disable-next-line no-param-reassign + event.target.value = null + } + }) + + switch (uploader) { + case "tus": + uppy.use(Tus, { endpoint: endpoint, limit: 6 }) + break + default: + } + + uppy.on("complete", (result) => { + if (result.failed.length === 0) { + console.log("Upload successful üòÄ") + } else { + console.warn("Upload failed üòû") + } + console.log("successful files:", result.successful) + console.log("failed files:", result.failed) + }) + + const setStateCb = () => { + setState(uppy.getState()) + } + + for (const event of LISTENING_EVENTS) { + uppy.on(event, setStateCb) + } + + return () => { + for (const event of ["complete", ...LISTENING_EVENTS]) { + uppyInstance.current?.off( + event as "complete" & keyof typeof LISTENING_EVENTS, + //@ts-expect-error -- huh? typescript wtf + setStateCb + ) + } + uppyInstance.current?.close() + uppyInstance.current = undefined + } + }, [targetRef, endpoint, uploader]) + + return { + files: uppyInstance.current?.getFiles() ?? [], + error: uppyInstance.current?.getState, + state, + upload: () => + uppyInstance.current?.upload() ?? + new Error("[useUppy] Uppy has not initialized yet."), + getInputProps: () => inputProps, + getRootProps, + removeFile, + cancelAll, + } +} diff --git a/app/components/ui/button.tsx b/app/components/ui/button.tsx index 1ce827f..7cabeb0 100644 --- a/app/components/ui/button.tsx +++ b/app/components/ui/button.tsx @@ -23,7 +23,7 @@ const buttonVariants = cva( size: { default: "h-9 px-4 py-2", sm: "h-8 rounded-md px-3 text-xs", - lg: "h-10 rounded-md px-8", + lg: "h-16 rounded-md px-8", icon: "h-9 w-9", }, }, diff --git a/app/components/ui/dialog.tsx b/app/components/ui/dialog.tsx new file mode 100644 index 0000000..9ed1e97 --- /dev/null +++ b/app/components/ui/dialog.tsx @@ -0,0 +1,120 @@ +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { Cross2Icon } from "@radix-ui/react-icons" + +import { cn } from "~/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/app/components/ui/progress.tsx b/app/components/ui/progress.tsx new file mode 100644 index 0000000..7fdf89f --- /dev/null +++ b/app/components/ui/progress.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import * as ProgressPrimitive from "@radix-ui/react-progress" + +import { cn } from "~/utils" + +const Progress = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, value, ...props }, ref) => ( + + + +)) +Progress.displayName = ProgressPrimitive.Root.displayName + +export { Progress } diff --git a/app/routes/dashboard.tsx b/app/routes/dashboard.tsx index 735e4d0..4b3280b 100644 --- a/app/routes/dashboard.tsx +++ b/app/routes/dashboard.tsx @@ -12,3 +12,4 @@ export default function Dashboard() { ) } + diff --git a/app/tailwind.css b/app/tailwind.css index 4c87031..477e95a 100644 --- a/app/tailwind.css +++ b/app/tailwind.css @@ -16,6 +16,7 @@ --primary: 242 51% 14%; --primary-1: 241 90% 82%; --primary-2: 241 21% 42%; + --primary-dark: 240 33% 4%; --primary-foreground: 0 0% 88%; --primary-1-foreground: 240 50% 9%; diff --git a/package-lock.json b/package-lock.json index 5132c25..ff27b6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,23 +1,29 @@ { - "name": "portal-dashboard", + "name": "lume-portal-dashboard", "lockfileVersion": 3, "requires": true, "packages": { "": { + "name": "lume-portal-dashboard", "dependencies": { "@conform-to/react": "^1.0.2", "@conform-to/zod": "^1.0.2", "@fontsource-variable/manrope": "^5.0.19", "@lumeweb/portal-sdk": "^0.0.0-20240306231947", "@radix-ui/react-checkbox": "^1.0.4", + "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-progress": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", "@refinedev/cli": "^2.16.1", "@refinedev/core": "^4.47.2", "@refinedev/remix-router": "^3.0.0", "@remix-run/node": "^2.8.0", "@remix-run/react": "^2.8.0", + "@uppy/core": "^3.9.3", + "@uppy/tus": "^3.5.3", + "@uppy/utils": "^5.7.4", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "react": "^18.2.0", @@ -3672,6 +3678,111 @@ } } }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz", + "integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.4", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-controllable-state": "1.0.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz", + "integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-escape-keydown": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", + "integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz", + "integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-icons": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.0.tgz", @@ -3680,6 +3791,24 @@ "react": "^16.x || ^17.x || ^18.x" } }, + "node_modules/@radix-ui/react-id": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", + "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-label": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.0.2.tgz", @@ -3703,6 +3832,29 @@ } } }, + "node_modules/@radix-ui/react-portal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz", + "integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-presence": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", @@ -3750,6 +3902,30 @@ } } }, + "node_modules/@radix-ui/react-progress": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.0.3.tgz", + "integrity": "sha512-5G6Om/tYSxjSeEdrb1VfKkfZfn/1IlPWd731h2RfPuSbIfNUgfqAwbKfJCg/PP6nuUCTrYzalwHSpSinoWoCag==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", @@ -3803,6 +3979,24 @@ } } }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz", + "integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-layout-effect": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", @@ -4907,6 +5101,11 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@transloadit/prettier-bytes": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@transloadit/prettier-bytes/-/prettier-bytes-0.3.1.tgz", + "integrity": "sha512-TozlXhWmBH1e2ZcQoz9tKuEl9dsvbS6uUghvcx/oluwN1XdcPHyF6+sZ3WhPv+FxO9j6dX7I3UV651mvJCbiQA==" + }, "node_modules/@types/acorn": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.6.tgz", @@ -5033,6 +5232,11 @@ "@types/react": "*" } }, + "node_modules/@types/retry": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==" + }, "node_modules/@types/scheduler": { "version": "0.16.8", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", @@ -5350,6 +5554,78 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, + "node_modules/@uppy/companion-client": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@uppy/companion-client/-/companion-client-3.7.4.tgz", + "integrity": "sha512-mBQ9dFZeobPAI3p1VdhGAgjnTGqMKVmW37f/q84Qz9TaxGplQFzYJGYR8/9aoj00ickMtzKtkKtrTAyFxgwHmw==", + "dependencies": { + "@uppy/utils": "^5.7.4", + "namespace-emitter": "^2.0.1", + "p-retry": "^6.1.0" + }, + "peerDependencies": { + "@uppy/core": "^3.9.3" + } + }, + "node_modules/@uppy/core": { + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/@uppy/core/-/core-3.9.3.tgz", + "integrity": "sha512-sUgiJ9Ag3eq8qf3i1unn7BOpjcidBj2lwnpJ8xlMGRcnQqwNznBXP2m39tK8nWEFvrj4kPgIslMZyoR0Lelzxg==", + "dependencies": { + "@transloadit/prettier-bytes": "^0.3.0", + "@uppy/store-default": "^3.2.2", + "@uppy/utils": "^5.7.4", + "lodash": "^4.17.21", + "mime-match": "^1.0.2", + "namespace-emitter": "^2.0.1", + "nanoid": "^4.0.0", + "preact": "^10.5.13" + } + }, + "node_modules/@uppy/core/node_modules/nanoid": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.2.tgz", + "integrity": "sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^14 || ^16 || >=18" + } + }, + "node_modules/@uppy/store-default": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@uppy/store-default/-/store-default-3.2.2.tgz", + "integrity": "sha512-OiSgT++Jj4nLK0N9WTeod3UNjCH81OXE5BcMJCd9oWzl2d0xPNq2T/E9Y6O72XVd+6Y7+tf5vZlPElutfMB3KQ==" + }, + "node_modules/@uppy/tus": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@uppy/tus/-/tus-3.5.3.tgz", + "integrity": "sha512-YUdeIZquFgnLfssZrweqf24Iui3+zYCaFC36W21/lmeWZl5oPUioICvoBUFOV3q7jnqpsK4pAUMQfR4KH04Plg==", + "dependencies": { + "@uppy/companion-client": "^3.7.3", + "@uppy/utils": "^5.7.3", + "tus-js-client": "^3.1.3" + }, + "peerDependencies": { + "@uppy/core": "^3.9.2" + } + }, + "node_modules/@uppy/utils": { + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/@uppy/utils/-/utils-5.7.4.tgz", + "integrity": "sha512-0Xsr7Xqdrb9mgfY3hi0YdhIaAxUw6qJasAflNMwNsyLGt3kH4pLfQHucolBKfWglVGtk1vfb49hZYvJGpcpzYA==", + "dependencies": { + "lodash": "^4.17.21", + "preact": "^10.5.13" + } + }, "node_modules/@vanilla-extract/babel-plugin-debug-ids": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@vanilla-extract/babel-plugin-debug-ids/-/babel-plugin-debug-ids-1.0.5.tgz", @@ -5633,6 +5909,17 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/aria-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.3.tgz", + "integrity": "sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", @@ -6800,6 +7087,24 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/combine-errors": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/combine-errors/-/combine-errors-3.0.3.tgz", + "integrity": "sha512-C8ikRNRMygCwaTx+Ek3Yr+OuZzgZjduCOfSQBjbM8V3MfgcjSTeto/GXP6PAwKvJz/v15b7GHZvx5rOlczFw/Q==", + "dependencies": { + "custom-error-instance": "2.1.1", + "lodash.uniqby": "4.5.0" + } + }, + "node_modules/combine-errors/node_modules/lodash.uniqby": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.5.0.tgz", + "integrity": "sha512-IRt7cfTtHy6f1aRVA5n7kT8rgN3N1nH6MOWLcHfpWG2SH19E3JksLK38MktLxZDhlAjCP9jpIXkOnRXlu6oByQ==", + "dependencies": { + "lodash._baseiteratee": "~4.7.0", + "lodash._baseuniq": "~4.6.0" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -7013,6 +7318,11 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "node_modules/custom-error-instance": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/custom-error-instance/-/custom-error-instance-2.1.1.tgz", + "integrity": "sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg==" + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -7216,6 +7526,11 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -9206,6 +9521,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/get-port": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", @@ -9906,6 +10229,14 @@ "node": ">= 0.4" } }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -10219,6 +10550,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-network-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.0.1.tgz", + "integrity": "sha512-OwQXkwBJeESyhFw+OumbJVD58BFBJJI5OM5S1+eyrDKlgDZPX2XNT5gXS56GSD3NPbbwUuMlR1Q71SRp5SobuQ==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -10499,6 +10841,11 @@ "jiti": "bin/jiti.js" } }, + "node_modules/js-base64": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz", + "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -11003,6 +11350,46 @@ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" }, + "node_modules/lodash._baseiteratee": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash._baseiteratee/-/lodash._baseiteratee-4.7.0.tgz", + "integrity": "sha512-nqB9M+wITz0BX/Q2xg6fQ8mLkyfF7MU7eE+MNBNjTHFKeKaZAPEzEg+E8LWxKWf1DQVflNEn9N49yAuqKh2mWQ==", + "dependencies": { + "lodash._stringtopath": "~4.8.0" + } + }, + "node_modules/lodash._basetostring": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-4.12.0.tgz", + "integrity": "sha512-SwcRIbyxnN6CFEEK4K1y+zuApvWdpQdBHM/swxP962s8HIxPO3alBH5t3m/dl+f4CMUug6sJb7Pww8d13/9WSw==" + }, + "node_modules/lodash._baseuniq": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz", + "integrity": "sha512-Ja1YevpHZctlI5beLA7oc5KNDhGcPixFhcqSiORHNsp/1QTv7amAXzw+gu4YOvErqVlMVyIJGgtzeepCnnur0A==", + "dependencies": { + "lodash._createset": "~4.0.0", + "lodash._root": "~3.0.0" + } + }, + "node_modules/lodash._createset": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/lodash._createset/-/lodash._createset-4.0.3.tgz", + "integrity": "sha512-GTkC6YMprrJZCYU3zcqZj+jkXkrXzq3IPBcF/fIPpNEAB4hZEtXU8zp/RwKOvZl43NUmwDbyRk3+ZTbeRdEBXA==" + }, + "node_modules/lodash._root": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz", + "integrity": "sha512-O0pWuFSK6x4EXhM1dhZ8gchNtG7JMqBtrHdoUFUWXD7dJnNSUze1GuyQr5sOs0aCvgGeI3o/OJW8f4ca7FDxmQ==" + }, + "node_modules/lodash._stringtopath": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/lodash._stringtopath/-/lodash._stringtopath-4.8.0.tgz", + "integrity": "sha512-SXL66C731p0xPDC5LZg4wI5H+dJo/EO4KTqOMwLYCH3+FmmfAKJEZCm6ohGpI+T1xwsDsJCfL4OnhorllvlTPQ==", + "dependencies": { + "lodash._basetostring": "~4.12.0" + } + }, "node_modules/lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", @@ -11050,6 +11437,11 @@ "resolved": "https://registry.npmjs.org/lodash.omitby/-/lodash.omitby-4.6.0.tgz", "integrity": "sha512-5OrRcIVR75M288p4nbI2WLAf3ndw2GD9fyNv3Bc15+WCxJDdZ4lYndSxGd7hnG6PVjiJTeJE2dHEGhIuKGicIQ==" }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==" + }, "node_modules/lodash.topath": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz", @@ -12150,6 +12542,14 @@ "node": ">= 0.6" } }, + "node_modules/mime-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mime-match/-/mime-match-1.0.2.tgz", + "integrity": "sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==", + "dependencies": { + "wildcard": "^1.1.0" + } + }, "node_modules/mime-types": { "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", @@ -12416,6 +12816,11 @@ "thenify-all": "^1.0.0" } }, + "node_modules/namespace-emitter": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/namespace-emitter/-/namespace-emitter-2.0.1.tgz", + "integrity": "sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==" + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -13196,6 +13601,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-retry": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.0.tgz", + "integrity": "sha512-JA6nkq6hKyWLLasXQXUrO4z8BUZGUt/LjlJxx8Gb2+2ntodU/SS63YZ8b0LUTbQ8ZB9iwOfhEPhg4ykKnn2KsA==", + "dependencies": { + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "engines": { + "node": ">= 4" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -13817,6 +14246,15 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, + "node_modules/preact": { + "version": "10.19.6", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.19.6.tgz", + "integrity": "sha512-gympg+T2Z1fG1unB8NH29yHJwnEaCH37Z32diPDku316OTnRPeMbiRV9kTrfZpocXjdfnWuFUl/Mj4BHaf6gnw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/preferred-pm": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/preferred-pm/-/preferred-pm-3.1.3.tgz", @@ -13930,6 +14368,16 @@ "react-is": "^16.13.1" } }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, "node_modules/property-information": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.4.1.tgz", @@ -14017,6 +14465,11 @@ "node": ">=0.10.0" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -14125,6 +14578,51 @@ "node": ">=0.10.0" } }, + "node_modules/react-remove-scroll": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", + "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", + "dependencies": { + "react-remove-scroll-bar": "^2.3.3", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.5.tgz", + "integrity": "sha512-3cqjOqg6s0XbOjWvmasmqHch+RLxIEk2r/70rzGXuz3iIGQsQheEQyqYCBb5EECoD01Vo2SIbDqW4paLeLTASw==", + "dependencies": { + "react-style-singleton": "^2.2.1", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-router": { "version": "6.22.2", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.2.tgz", @@ -14155,6 +14653,28 @@ "react-dom": ">=16.8" } }, + "node_modules/react-style-singleton": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", + "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", + "dependencies": { + "get-nonce": "^1.0.0", + "invariant": "^2.2.4", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-textarea-autosize": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.3.tgz", @@ -14606,7 +15126,6 @@ "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, "engines": { "node": ">= 4" } @@ -16315,6 +16834,20 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, + "node_modules/tus-js-client": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/tus-js-client/-/tus-js-client-3.1.3.tgz", + "integrity": "sha512-n9k6rI/nPOuP2TaqPG6Ogz3a3V1cSH9en7N0VH4gh95jmG8JA58TJzLms2lBfb7aKVb3fdUunqYEG3WnQnZRvQ==", + "dependencies": { + "buffer-from": "^1.1.2", + "combine-errors": "^3.0.3", + "is-stream": "^2.0.0", + "js-base64": "^3.7.2", + "lodash.throttle": "^4.1.1", + "proper-lockfile": "^4.1.2", + "url-parse": "^1.5.7" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -16849,6 +17382,15 @@ "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", "deprecated": "Please see https://github.com/lydell/urix#deprecated" }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", @@ -16857,6 +17399,26 @@ "node": ">=0.10.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.1.tgz", + "integrity": "sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-composed-ref": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.3.0.tgz", @@ -16894,6 +17456,27 @@ } } }, + "node_modules/use-sidecar": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", + "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", @@ -17708,6 +18291,11 @@ "node": ">=8" } }, + "node_modules/wildcard": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-1.1.2.tgz", + "integrity": "sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==" + }, "node_modules/wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", diff --git a/package.json b/package.json index 8b32474..f424d25 100644 --- a/package.json +++ b/package.json @@ -16,14 +16,19 @@ "@fontsource-variable/manrope": "^5.0.19", "@lumeweb/portal-sdk": "^0.0.0-20240306231947", "@radix-ui/react-checkbox": "^1.0.4", + "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-progress": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", "@refinedev/cli": "^2.16.1", "@refinedev/core": "^4.47.2", "@refinedev/remix-router": "^3.0.0", "@remix-run/node": "^2.8.0", "@remix-run/react": "^2.8.0", + "@uppy/core": "^3.9.3", + "@uppy/tus": "^3.5.3", + "@uppy/utils": "^5.7.4", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "react": "^18.2.0", diff --git a/tailwind.config.ts b/tailwind.config.ts index 24252ad..baba10b 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -20,7 +20,7 @@ const config = { border: "hsl(var(--border))", input: { DEFAULT: "hsl(var(--input))", - placeholder: "hsl(var(--input-placeholder))", + placeholder: "hsl(var(--input-placeholder))" }, ring: "hsl(var(--ring))", background: "hsl(var(--background))", @@ -34,7 +34,10 @@ const config = { foreground: "hsl(var(--primary-1-foreground))" }, "primary-2": { - DEFAULT: "hsl(var(--primary-2))", + DEFAULT: "hsl(var(--primary-2))" + }, + "primary-dark": { + DEFAULT: "hsl(var(--primary-dark))" }, secondary: { DEFAULT: "hsl(var(--secondary))", diff --git a/vite.config.ts b/vite.config.ts index 41db512..09fba71 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,14 +1,14 @@ -import { vitePlugin as remix } from "@remix-run/dev"; -import { defineConfig } from "vite"; -import tsconfigPaths from "vite-tsconfig-paths"; +import { vitePlugin as remix } from "@remix-run/dev" +import { defineConfig } from "vite" +import tsconfigPaths from "vite-tsconfig-paths" export default defineConfig({ plugins: [ remix({ ssr: false, - ignoredRouteFiles: ["**/*.css"], + ignoredRouteFiles: ["**/*.css"] }), - tsconfigPaths(), + tsconfigPaths() ], server: { fs: { @@ -18,7 +18,10 @@ export default defineConfig({ // If you're comfortable with Vite's dev server making any file within the // project root available, you can remove this option. See more: // https://vitejs.dev/config/server-options.html#server-fs-allow - allow: ["app", "node_modules/@fontsource-variable/manrope"], - }, - }, -}); + allow: [ + "app", + "node_modules/@fontsource-variable/manrope", + ] + } + } +}) From 94406b7f13f6f49afbadf3caa82378b2053b7d0d Mon Sep 17 00:00:00 2001 From: Juan Di Toro Date: Tue, 12 Mar 2024 15:32:00 +0100 Subject: [PATCH 2/3] fix: state and rendering --- app/components/dashboard-layout.tsx | 173 ++++++++++++++-------------- app/components/lib/uppy.ts | 52 ++++++--- 2 files changed, 125 insertions(+), 100 deletions(-) diff --git a/app/components/dashboard-layout.tsx b/app/components/dashboard-layout.tsx index 422d765..66c6c01 100644 --- a/app/components/dashboard-layout.tsx +++ b/app/components/dashboard-layout.tsx @@ -54,7 +54,17 @@ export const DashboardLayout = ({ children }: React.PropsWithChildren<{}>) => {

Privacy

Ownership

- + + + + + + + + {children} @@ -89,97 +99,92 @@ export const DashboardLayout = ({ children }: React.PropsWithChildren<{}>) => { ) } -const UploadFileModal = () => { - const { getRootProps, getInputProps, files, upload, removeFile, cancelAll } = - useUppy({ - uploader: "tus", - endpoint: import.meta.env.VITE_PUBLIC_TUS_ENDPOINT - }) +const UploadFileForm = () => { + const { + getRootProps, + getInputProps, + getFiles, + upload, + state, + removeFile, + cancelAll + } = useUppy({ + uploader: "tus", + endpoint: import.meta.env.VITE_PUBLIC_TUS_ENDPOINT + }) - const isUploading = - files.length > 0 - ? files.map((file) => file.progress?.uploadStarted != null).includes(true) - : false - const isCompleted = - files.length > 0 - ? files - .map((file) => file.progress?.uploadComplete) - .reduce((acc, cur) => acc && cur, true) - : false - const hasStarted = isUploading || isCompleted + console.log({ state, files: getFiles() }) + + const isUploading = state === "uploading" + const isCompleted = state === "completed" + const hasStarted = state !== "idle" && state !== "initializing" return ( - - - - - - - Upload Files - - {!hasStarted ? ( -
- - -

Drag & Drop Files or Browse

-
- ) : null} - -
- {files.map((file) => ( - { - removeFile(id) - }} - /> - ))} + <> + + Upload Files + + {!hasStarted ? ( +
+ + +

Drag & Drop Files or Browse

+ ) : null} - {hasStarted ? ( -
- - {isCompleted - ? "Upload completed" - : `${files.length} being uploaded`} -
- ) : null} +
+ {getFiles().map((file) => ( + { + removeFile(id) + }} + /> + ))} +
- {hasStarted ? ( - - - - ) : ( - - )} + + ) : null} - {hasStarted && isCompleted ? ( - - - - ) : null} - -
+ {isCompleted ? ( + + + + ) : null} + + {!hasStarted && !isCompleted && !isUploading ? ( + + ) : null} + ) } diff --git a/app/components/lib/uppy.ts b/app/components/lib/uppy.ts index 807b695..9c06abb 100644 --- a/app/components/lib/uppy.ts +++ b/app/components/lib/uppy.ts @@ -36,7 +36,10 @@ export function useUppy({ (element: HTMLElement | null) => _setTargetRef(element), [] ) - const [state, setState] = useState() + const [uppyState, setUppyState] = useState() + const [state, setState] = useState< + "completed" | "idle" | "initializing" | "error" | "uploading" + >("initializing") const [inputProps, setInputProps] = useState< | { @@ -71,7 +74,7 @@ export function useUppy({ [targetRef, uppyInstance] ) const cancelAll = useCallback( - () => uppyInstance.current?.cancelAll(), + () => uppyInstance.current?.cancelAll({ reason: "user" }), // eslint-disable-next-line react-hooks/exhaustive-deps [targetRef, uppyInstance] ) @@ -116,36 +119,53 @@ export function useUppy({ uppy.on("complete", (result) => { if (result.failed.length === 0) { console.log("Upload successful üòÄ") + setState("completed") } else { console.warn("Upload failed üòû") + setState("error") } console.log("successful files:", result.successful) console.log("failed files:", result.failed) }) - const setStateCb = () => { - setState(uppy.getState()) + const setStateCb = (event: (typeof LISTENING_EVENTS)[number]) => { + switch (event) { + case "upload": + setState("uploading") + break + case "upload-error": + setState("error") + break + default: + break + } + setUppyState(uppy.getState()) } for (const event of LISTENING_EVENTS) { - uppy.on(event, setStateCb) + uppy.on(event, function cb() { + setStateCb(event) + }) } + setState("idle") return () => { - for (const event of ["complete", ...LISTENING_EVENTS]) { - uppyInstance.current?.off( - event as "complete" & keyof typeof LISTENING_EVENTS, - //@ts-expect-error -- huh? typescript wtf - setStateCb - ) - } - uppyInstance.current?.close() - uppyInstance.current = undefined + // for (const event of ["complete", ...LISTENING_EVENTS]) { + // uppyInstance.current?.off( + // event as "complete" & keyof typeof LISTENING_EVENTS, + // //@ts-expect-error -- huh? typescript wtf + // setStateCb + // ) + // } + // uppyInstance.current?.cancelAll({ reason: "unmount" }) + // uppyInstance.current?.logout() + // uppyInstance.current?.close() + // uppyInstance.current = undefined } }, [targetRef, endpoint, uploader]) return { - files: uppyInstance.current?.getFiles() ?? [], + getFiles: () => uppyInstance.current?.getFiles() ?? [], error: uppyInstance.current?.getState, state, upload: () => @@ -154,6 +174,6 @@ export function useUppy({ getInputProps: () => inputProps, getRootProps, removeFile, - cancelAll, + cancelAll } } From 0ba128b135b39ef5fceceba588bb60c123b14114 Mon Sep 17 00:00:00 2001 From: Juan Di Toro Date: Tue, 12 Mar 2024 15:36:44 +0100 Subject: [PATCH 3/3] fix: added cleanup when component unmount --- app/components/lib/uppy.ts | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/app/components/lib/uppy.ts b/app/components/lib/uppy.ts index 9c06abb..147fe04 100644 --- a/app/components/lib/uppy.ts +++ b/app/components/lib/uppy.ts @@ -148,22 +148,16 @@ export function useUppy({ }) } setState("idle") - - return () => { - // for (const event of ["complete", ...LISTENING_EVENTS]) { - // uppyInstance.current?.off( - // event as "complete" & keyof typeof LISTENING_EVENTS, - // //@ts-expect-error -- huh? typescript wtf - // setStateCb - // ) - // } - // uppyInstance.current?.cancelAll({ reason: "unmount" }) - // uppyInstance.current?.logout() - // uppyInstance.current?.close() - // uppyInstance.current = undefined - } }, [targetRef, endpoint, uploader]) + useEffect(() => { + return () => { + uppyInstance.current?.cancelAll({ reason: "unmount" }) + uppyInstance.current?.logout() + uppyInstance.current?.close() + uppyInstance.current = undefined + } + }, []) return { getFiles: () => uppyInstance.current?.getFiles() ?? [], error: uppyInstance.current?.getState,