Merge branch 'develop' into riobuenoDevelops/dashboard
This commit is contained in:
commit
ddc7628023
|
@ -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 GeneralLayout = ({ children }: React.PropsWithChildren<{}>) => {
|
||||
return (
|
||||
|
@ -39,14 +50,21 @@ export const GeneralLayout = ({ children }: React.PropsWithChildren<{}>) => {
|
|||
</ul>
|
||||
</nav>
|
||||
<span className="text-primary-2 mb-3 -space-y-1 opacity-40">
|
||||
<p>Freedom</p>
|
||||
<p>Privacy</p>
|
||||
<p>Ownership</p>
|
||||
<p>Freedom</p>
|
||||
<p>Privacy</p>
|
||||
<p>Ownership</p>
|
||||
</span>
|
||||
<Button className="w-[calc(100%-3rem)] font-semibold h-16">
|
||||
<CloudUploadIcon className="w-6 h-6 -ml-3 mr-4" />
|
||||
Upload Files
|
||||
</Button>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button size={"lg"} className="w-[calc(100%-3rem)] font-semibold">
|
||||
<CloudUploadIcon className="w-6 h-6 -ml-3 mr-4" />
|
||||
Upload Files
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<UploadFileForm />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</header>
|
||||
|
||||
<div className="flex-1">
|
||||
|
@ -83,9 +101,147 @@ export const GeneralLayout = ({ children }: React.PropsWithChildren<{}>) => {
|
|||
)
|
||||
}
|
||||
|
||||
const UploadFileForm = () => {
|
||||
const {
|
||||
getRootProps,
|
||||
getInputProps,
|
||||
getFiles,
|
||||
upload,
|
||||
state,
|
||||
removeFile,
|
||||
cancelAll
|
||||
} = useUppy({
|
||||
uploader: "tus",
|
||||
endpoint: import.meta.env.VITE_PUBLIC_TUS_ENDPOINT
|
||||
})
|
||||
|
||||
console.log({ state, files: getFiles() })
|
||||
|
||||
const isUploading = state === "uploading"
|
||||
const isCompleted = state === "completed"
|
||||
const hasStarted = state !== "idle" && state !== "initializing"
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogHeader className="mb-6">
|
||||
<DialogTitle>Upload Files</DialogTitle>
|
||||
</DialogHeader>
|
||||
{!hasStarted ? (
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className="border border-border rounded text-primary-2 bg-primary-dark h-48 flex flex-col items-center justify-center"
|
||||
>
|
||||
<input
|
||||
hidden
|
||||
aria-hidden
|
||||
name="uppyFiles[]"
|
||||
key={new Date().toISOString()}
|
||||
multiple
|
||||
{...getInputProps()}
|
||||
/>
|
||||
<CloudUploadIcon className="w-24 h-24 stroke stroke-primary-dark" />
|
||||
<p>Drag & Drop Files or Browse</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="w-full space-y-3 max-h-48 overflow-y-auto">
|
||||
{getFiles().map((file) => (
|
||||
<UploadFileItem
|
||||
key={file.id}
|
||||
file={file}
|
||||
onRemove={(id) => {
|
||||
removeFile(id)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{hasStarted ? (
|
||||
<div className="flex flex-col items-center gap-y-2 w-full text-primary-1">
|
||||
<CloudCheckIcon className="w-32 h-32" />
|
||||
{isCompleted
|
||||
? "Upload completed"
|
||||
: `${getFiles().length} files being uploaded`}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{isUploading ? (
|
||||
<DialogClose asChild onClick={cancelAll}>
|
||||
<Button size={"lg"} className="mt-6">
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogClose>
|
||||
) : null}
|
||||
|
||||
{isCompleted ? (
|
||||
<DialogClose asChild>
|
||||
<Button size={"lg"} className="mt-6">
|
||||
Close
|
||||
</Button>
|
||||
</DialogClose>
|
||||
) : null}
|
||||
|
||||
{!hasStarted && !isCompleted && !isUploading ? (
|
||||
<Button size={"lg"} className="mt-6" onClick={upload}>
|
||||
Upload
|
||||
</Button>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function bytestoMegabytes(bytes: number) {
|
||||
return bytes / 1024 / 1024
|
||||
}
|
||||
|
||||
const UploadFileItem = ({
|
||||
file,
|
||||
onRemove
|
||||
}: {
|
||||
file: UppyFile
|
||||
onRemove: (id: string) => void
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col w-full py-4 px-2 bg-primary-dark">
|
||||
<div className="flex text-primary-1 items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<div className="p-2">
|
||||
{file.progress?.uploadComplete ? (
|
||||
<BoxCheckedIcon className="w-4 h-4" />
|
||||
) : (
|
||||
<PageIcon className="w-4 h-4" />
|
||||
)}
|
||||
</div>
|
||||
<p className="w-full flex justify-between items-center">
|
||||
<span className="truncate text-ellipsis max-w-[30ch]">
|
||||
{file.name}
|
||||
</span>{" "}
|
||||
<span>({bytestoMegabytes(file.size).toFixed(2)} MB)</span>
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
size={"icon"}
|
||||
variant={"ghost"}
|
||||
className="!text-inherit"
|
||||
onClick={() => onRemove(file.id)}
|
||||
>
|
||||
<TrashIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{file.progress?.uploadStarted && !file.progress.uploadComplete ? (
|
||||
<Progress value={file.progress.percentage} className="mt-2" />
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const NavigationButton = ({ children }: React.PropsWithChildren) => {
|
||||
return (
|
||||
<Button variant="ghost" className="justify-start h-14 w-[calc(100%-3rem)] font-semibold">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="justify-start h-14 w-[calc(100%-3rem)] font-semibold"
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
)
|
||||
|
@ -166,3 +322,143 @@ const CloudUploadIcon = ({ className }: { className?: string }) => {
|
|||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const PageIcon = ({ className }: { className?: string }) => {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
width="21"
|
||||
height="21"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
d="M10.3276 4.21337V15.0287H0.59375V0.96875H5.46067"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M5.46191 0.96875L10.3288 4.21337"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M5.46191 0.96875V4.21337"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M10.3288 4.21289H5.46191"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M2.75684 8.53906H5.46068"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M2.75684 10.7021H7.62376"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const TrashIcon = ({ className }: { className?: string }) => {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
width="21"
|
||||
height="21"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
d="M10.0769 14.3093H2.69194C2.3247 14.3093 1.9725 14.1634 1.71282 13.9037C1.45314 13.6441 1.30725 13.2919 1.30725 12.9246V4.15492C1.30725 4.03251 1.35588 3.91511 1.44244 3.82855C1.529 3.74199 1.6464 3.69336 1.76881 3.69336C1.89123 3.69336 2.00863 3.74199 2.09519 3.82855C2.18175 3.91511 2.23038 4.03251 2.23038 4.15492V12.9246C2.23038 13.047 2.279 13.1644 2.36556 13.251C2.45212 13.3375 2.56952 13.3862 2.69194 13.3862H10.0769C10.1994 13.3862 10.3168 13.3375 10.4033 13.251C10.4899 13.1644 10.5385 13.047 10.5385 12.9246V4.15492C10.5385 4.03251 10.5871 3.91511 10.6737 3.82855C10.7602 3.74199 10.8776 3.69336 11.0001 3.69336C11.1225 3.69336 11.2399 3.74199 11.3264 3.82855C11.413 3.91511 11.4616 4.03251 11.4616 4.15492V12.9246C11.4616 13.2919 11.3157 13.6441 11.0561 13.9037C10.7964 14.1634 10.4442 14.3093 10.0769 14.3093Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M11.9237 3.23172H0.846206C0.723792 3.23172 0.606392 3.18309 0.519832 3.09653C0.433272 3.00997 0.384644 2.89257 0.384644 2.77016C0.384644 2.64774 0.433272 2.53034 0.519832 2.44378C0.606392 2.35722 0.723792 2.30859 0.846206 2.30859H11.9237C12.0461 2.30859 12.1635 2.35722 12.2501 2.44378C12.3366 2.53034 12.3853 2.64774 12.3853 2.77016C12.3853 2.89257 12.3366 3.00997 12.2501 3.09653C12.1635 3.18309 12.0461 3.23172 11.9237 3.23172Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M8.23121 3.23129C8.1088 3.23129 7.9914 3.18266 7.90484 3.0961C7.81828 3.00954 7.76965 2.89214 7.76965 2.76973V1.38504H5.00027V2.76973C5.00027 2.89214 4.95164 3.00954 4.86508 3.0961C4.77853 3.18266 4.66112 3.23129 4.53871 3.23129C4.4163 3.23129 4.2989 3.18266 4.21234 3.0961C4.12578 3.00954 4.07715 2.89214 4.07715 2.76973V0.923477C4.07715 0.801063 4.12578 0.683662 4.21234 0.597103C4.2989 0.510543 4.4163 0.461914 4.53871 0.461914H8.23121C8.35362 0.461914 8.47103 0.510543 8.55759 0.597103C8.64415 0.683662 8.69277 0.801063 8.69277 0.923477V2.76973C8.69277 2.89214 8.64415 3.00954 8.55759 3.0961C8.47103 3.18266 8.35362 3.23129 8.23121 3.23129Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M6.3849 12.0012C6.26249 12.0012 6.14509 11.9526 6.05853 11.866C5.97197 11.7795 5.92334 11.6621 5.92334 11.5396V5.07777C5.92334 4.95536 5.97197 4.83796 6.05853 4.7514C6.14509 4.66484 6.26249 4.61621 6.3849 4.61621C6.50732 4.61621 6.62472 4.66484 6.71128 4.7514C6.79784 4.83796 6.84646 4.95536 6.84646 5.07777V11.5396C6.84646 11.6621 6.79784 11.7795 6.71128 11.866C6.62472 11.9526 6.50732 12.0012 6.3849 12.0012Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M8.69313 11.0778C8.57072 11.0778 8.45332 11.0292 8.36676 10.9426C8.2802 10.8561 8.23157 10.7387 8.23157 10.6163V6.00062C8.23157 5.87821 8.2802 5.76081 8.36676 5.67425C8.45332 5.58769 8.57072 5.53906 8.69313 5.53906C8.81554 5.53906 8.93294 5.58769 9.0195 5.67425C9.10606 5.76081 9.15469 5.87821 9.15469 6.00062V10.6163C9.15469 10.7387 9.10606 10.8561 9.0195 10.9426C8.93294 11.0292 8.81554 11.0778 8.69313 11.0778Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M4.07716 11.0778C3.95475 11.0778 3.83735 11.0292 3.75079 10.9426C3.66423 10.8561 3.6156 10.7387 3.6156 10.6163V6.00062C3.6156 5.87821 3.66423 5.76081 3.75079 5.67425C3.83735 5.58769 3.95475 5.53906 4.07716 5.53906C4.19958 5.53906 4.31698 5.58769 4.40354 5.67425C4.4901 5.76081 4.53873 5.87821 4.53873 6.00062V10.6163C4.53873 10.7387 4.4901 10.8561 4.40354 10.9426C4.31698 11.0292 4.19958 11.0778 4.07716 11.0778Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const CloudCheckIcon = ({ className }: { className?: string }) => {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
width="21"
|
||||
height="21"
|
||||
viewBox="0 0 72 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M58.2 18C56.1 7.8 47.1 0 36 0C27.3 0 19.8 4.8 16.2 12C6.9 13.2 0 20.7 0 30C0 39.9 8.1 48 18 48H57C65.4 48 72 41.4 72 33C72 25.2 65.7 18.6 58.2 18ZM30 39L19.5 28.5L23.7 24.3L30 30.6L45.6 15L49.8 19.2L30 39Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const BoxCheckedIcon = ({ className }: { className?: string }) => {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
width="21"
|
||||
height="21"
|
||||
viewBox="0 0 21 21"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5.5 4.5H15.5C16.6046 4.5 17.5 5.39543 17.5 6.5V16.5C17.5 17.6046 16.6046 18.5 15.5 18.5H5.5C4.39543 18.5 3.5 17.6046 3.5 16.5V6.5C3.5 5.39543 4.39543 4.5 5.5 4.5Z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M7.5 11.5L9.5 13.5L13.5 9.5"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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<DropTargetOptions>
|
||||
|
||||
type Meta = {
|
||||
relativePath?: string | null
|
||||
}
|
||||
type Body = IndexedObject<unknown>
|
||||
|
||||
// Default options
|
||||
const defaultOpts = {
|
||||
target: null,
|
||||
} satisfies DropTargetOptions
|
||||
|
||||
interface DragEventWithFileTransfer extends DragEvent {
|
||||
dataTransfer: NonNullable<DragEvent["dataTransfer"]>
|
||||
}
|
||||
|
||||
function isFileTransfer(event: DragEvent): event is DragEventWithFileTransfer {
|
||||
return event.dataTransfer?.types?.some((type) => type === "Files") ?? false
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop Target plugin
|
||||
*
|
||||
*/
|
||||
class DropTarget<M extends Meta, B extends Body> extends BasePlugin {
|
||||
static VERSION = "lume-internal"
|
||||
|
||||
private removeDragOverDataAttr: ReturnType<typeof setTimeout> | undefined
|
||||
private nodes?: Array<HTMLElement>
|
||||
|
||||
public opts: DropTargetOptions
|
||||
|
||||
constructor(uppy: Uppy<M, B>, 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<File>): 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<void> => {
|
||||
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
|
|
@ -0,0 +1,173 @@
|
|||
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<HTMLInputElement>(null)
|
||||
const [targetRef, _setTargetRef] = useState<HTMLElement | null>(null)
|
||||
const uppyInstance = useRef<Uppy>()
|
||||
const setRef = useCallback(
|
||||
(element: HTMLElement | null) => _setTargetRef(element),
|
||||
[]
|
||||
)
|
||||
const [uppyState, setUppyState] = useState<State>()
|
||||
const [state, setState] = useState<
|
||||
"completed" | "idle" | "initializing" | "error" | "uploading"
|
||||
>("initializing")
|
||||
|
||||
const [inputProps, setInputProps] = useState<
|
||||
| {
|
||||
ref: typeof inputRef
|
||||
type: "file"
|
||||
onChange: (event: ChangeEvent<HTMLInputElement>) => 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({ reason: "user" }),
|
||||
// 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 <input/> 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 üòÄ")
|
||||
setState("completed")
|
||||
} else {
|
||||
console.warn("Upload failed üòû")
|
||||
setState("error")
|
||||
}
|
||||
console.log("successful files:", result.successful)
|
||||
console.log("failed files:", result.failed)
|
||||
})
|
||||
|
||||
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, function cb() {
|
||||
setStateCb(event)
|
||||
})
|
||||
}
|
||||
setState("idle")
|
||||
}, [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,
|
||||
state,
|
||||
upload: () =>
|
||||
uppyInstance.current?.upload() ??
|
||||
new Error("[useUppy] Uppy has not initialized yet."),
|
||||
getInputProps: () => inputProps,
|
||||
getRootProps,
|
||||
removeFile,
|
||||
cancelAll
|
||||
}
|
||||
}
|
|
@ -25,7 +25,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",
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<Cross2Icon className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogHeader.displayName = "DialogHeader"
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogFooter.displayName = "DialogFooter"
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
}
|
|
@ -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%;
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -17,6 +17,7 @@
|
|||
"@lumeweb/portal-sdk": "^0.0.0-20240306231947",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@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",
|
||||
|
@ -29,6 +30,9 @@
|
|||
"@remix-run/react": "^2.8.0",
|
||||
"@tanstack/react-table": "^8.13.2",
|
||||
"@visx/visx": "^3.10.2",
|
||||
"@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",
|
||||
|
|
|
@ -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))",
|
||||
|
|
|
@ -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",
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue