add component
This commit is contained in:
parent
d97c41bd7f
commit
f6b87c4890
3 changed files with 363 additions and 7 deletions
202
frontend/components/ui/dropzone.tsx
Normal file
202
frontend/components/ui/dropzone.tsx
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
'use client';
|
||||
|
||||
import { UploadIcon } from 'lucide-react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { createContext, useContext } from 'react';
|
||||
import type { DropEvent, DropzoneOptions, FileRejection } from 'react-dropzone';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type DropzoneContextType = {
|
||||
src?: File[];
|
||||
accept?: DropzoneOptions['accept'];
|
||||
maxSize?: DropzoneOptions['maxSize'];
|
||||
minSize?: DropzoneOptions['minSize'];
|
||||
maxFiles?: DropzoneOptions['maxFiles'];
|
||||
};
|
||||
|
||||
const renderBytes = (bytes: number) => {
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(2)}${units[unitIndex]}`;
|
||||
};
|
||||
|
||||
const DropzoneContext = createContext<DropzoneContextType | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
export type DropzoneProps = Omit<DropzoneOptions, 'onDrop'> & {
|
||||
src?: File[];
|
||||
className?: string;
|
||||
onDrop?: (
|
||||
acceptedFiles: File[],
|
||||
fileRejections: FileRejection[],
|
||||
event: DropEvent
|
||||
) => void;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export const Dropzone = ({
|
||||
accept,
|
||||
maxFiles = 1,
|
||||
maxSize,
|
||||
minSize,
|
||||
onDrop,
|
||||
onError,
|
||||
disabled,
|
||||
src,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: DropzoneProps) => {
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
accept,
|
||||
maxFiles,
|
||||
maxSize,
|
||||
minSize,
|
||||
onError,
|
||||
disabled,
|
||||
onDrop: (acceptedFiles, fileRejections, event) => {
|
||||
if (fileRejections.length > 0) {
|
||||
const message = fileRejections.at(0)?.errors.at(0)?.message;
|
||||
onError?.(new Error(message));
|
||||
return;
|
||||
}
|
||||
|
||||
onDrop?.(acceptedFiles, fileRejections, event);
|
||||
},
|
||||
...props,
|
||||
});
|
||||
|
||||
return (
|
||||
<DropzoneContext.Provider
|
||||
key={JSON.stringify(src)}
|
||||
value={{ src, accept, maxSize, minSize, maxFiles }}
|
||||
>
|
||||
<Button
|
||||
className={cn(
|
||||
'relative h-auto w-full flex-col overflow-hidden p-8',
|
||||
isDragActive && 'outline-none ring-1 ring-ring',
|
||||
className
|
||||
)}
|
||||
disabled={disabled}
|
||||
type="button"
|
||||
variant="outline"
|
||||
{...getRootProps()}
|
||||
>
|
||||
<input {...getInputProps()} disabled={disabled} />
|
||||
{children}
|
||||
</Button>
|
||||
</DropzoneContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const useDropzoneContext = () => {
|
||||
const context = useContext(DropzoneContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useDropzoneContext must be used within a Dropzone');
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export type DropzoneContentProps = {
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const maxLabelItems = 3;
|
||||
|
||||
export const DropzoneContent = ({
|
||||
children,
|
||||
className,
|
||||
}: DropzoneContentProps) => {
|
||||
const { src } = useDropzoneContext();
|
||||
|
||||
if (!src) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (children) {
|
||||
return children;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col items-center justify-center', className)}>
|
||||
<div className="flex size-8 items-center justify-center rounded-md bg-muted text-muted-foreground">
|
||||
<UploadIcon size={16} />
|
||||
</div>
|
||||
<p className="my-2 w-full truncate font-medium text-sm">
|
||||
{src.length > maxLabelItems
|
||||
? `${new Intl.ListFormat('en').format(
|
||||
src.slice(0, maxLabelItems).map((file) => file.name)
|
||||
)} and ${src.length - maxLabelItems} more`
|
||||
: new Intl.ListFormat('en').format(src.map((file) => file.name))}
|
||||
</p>
|
||||
<p className="w-full text-wrap text-muted-foreground text-xs">
|
||||
Drag and drop or click to replace
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export type DropzoneEmptyStateProps = {
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const DropzoneEmptyState = ({
|
||||
children,
|
||||
className,
|
||||
}: DropzoneEmptyStateProps) => {
|
||||
const { src, accept, maxSize, minSize, maxFiles } = useDropzoneContext();
|
||||
|
||||
if (src) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (children) {
|
||||
return children;
|
||||
}
|
||||
|
||||
let caption = '';
|
||||
|
||||
if (accept) {
|
||||
caption += 'Accepts ';
|
||||
caption += new Intl.ListFormat('en').format(Object.keys(accept));
|
||||
}
|
||||
|
||||
if (minSize && maxSize) {
|
||||
caption += ` between ${renderBytes(minSize)} and ${renderBytes(maxSize)}`;
|
||||
} else if (minSize) {
|
||||
caption += ` at least ${renderBytes(minSize)}`;
|
||||
} else if (maxSize) {
|
||||
caption += ` less than ${renderBytes(maxSize)}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col items-center justify-center', className)}>
|
||||
<div className="flex size-8 items-center justify-center rounded-md bg-muted text-muted-foreground">
|
||||
<UploadIcon size={16} />
|
||||
</div>
|
||||
<p className="my-2 w-full truncate text-wrap font-medium text-sm">
|
||||
Upload {maxFiles === 1 ? 'a file' : 'files'}
|
||||
</p>
|
||||
<p className="w-full truncate text-wrap text-muted-foreground text-xs">
|
||||
Drag and drop or click to upload
|
||||
</p>
|
||||
{caption && (
|
||||
<p className="text-wrap text-muted-foreground text-xs">{caption}.</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
165
frontend/package-lock.json
generated
165
frontend/package-lock.json
generated
|
|
@ -23,7 +23,7 @@
|
|||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slider": "^1.3.6",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
|
|
@ -42,6 +42,7 @@
|
|||
"next-themes": "^0.4.6",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-hook-form": "^7.65.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
|
|
@ -1390,6 +1391,23 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
|
|
@ -1456,6 +1474,23 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-direction": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
|
||||
|
|
@ -1648,6 +1683,23 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-navigation-menu": {
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz",
|
||||
|
|
@ -1721,6 +1773,23 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popper": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
|
||||
|
|
@ -1824,6 +1893,23 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-radio-group": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz",
|
||||
|
|
@ -1930,6 +2016,23 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-separator": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz",
|
||||
|
|
@ -1987,10 +2090,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"license": "MIT",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz",
|
||||
"integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
|
|
@ -2097,6 +2199,23 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||
|
|
@ -3381,6 +3500,14 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/attr-accept": {
|
||||
"version": "2.2.5",
|
||||
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz",
|
||||
"integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/autoprefixer": {
|
||||
"version": "10.4.21",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
|
||||
|
|
@ -4890,6 +5017,17 @@
|
|||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/file-selector": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz",
|
||||
"integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
|
|
@ -6526,7 +6664,6 @@
|
|||
"version": "0.525.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.525.0.tgz",
|
||||
"integrity": "sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
|
|
@ -8349,6 +8486,22 @@
|
|||
"react": "^19.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dropzone": {
|
||||
"version": "14.3.8",
|
||||
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.8.tgz",
|
||||
"integrity": "sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==",
|
||||
"dependencies": {
|
||||
"attr-accept": "^2.2.4",
|
||||
"file-selector": "^2.1.0",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.8 || 18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-hook-form": {
|
||||
"version": "7.65.0",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.65.0.tgz",
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slider": "^1.3.6",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
|
|
@ -43,6 +43,7 @@
|
|||
"next-themes": "^0.4.6",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-hook-form": "^7.65.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue