'use client';
import {
FileArchiveIcon,
FileAudioIcon,
FileCodeIcon,
FileCogIcon,
FileIcon,
FileTextIcon,
FileVideoIcon,
} from 'lucide-react';
import { Slot as SlotPrimitive } from '@radix-ui/react-slot';
import { useDirection as useDirectionPrimitive } from '@radix-ui/react-direction';
import * as React from 'react';
import { cn } from '@djangocfg/ui-core/lib';
import { useComposedRefs } from '@djangocfg/ui-core/lib';
import type {
FileUploadProps,
FileUploadContextValue,
FileUploadItemContextValue,
FileState,
StoreState,
StoreAction,
Store,
} from './types';
const ROOT_NAME = 'FileUpload';
const DROPZONE_NAME = 'FileUploadDropzone';
const TRIGGER_NAME = 'FileUploadTrigger';
const LIST_NAME = 'FileUploadList';
const ITEM_NAME = 'FileUploadItem';
const ITEM_PREVIEW_NAME = 'FileUploadItemPreview';
const ITEM_METADATA_NAME = 'FileUploadItemMetadata';
const ITEM_PROGRESS_NAME = 'FileUploadItemProgress';
const ITEM_DELETE_NAME = 'FileUploadItemDelete';
const CLEAR_NAME = 'FileUploadClear';
function formatBytes(bytes: number) {
if (bytes === 0) return '0 B';
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${(bytes / 1024 ** i).toFixed(i ? 1 : 0)} ${sizes[i]}`;
}
function getFileIcon(file: File) {
const type = file.type;
const extension = file.name.split('.').pop()?.toLowerCase() ?? '';
if (type.startsWith('video/')) {
return ;
}
if (type.startsWith('audio/')) {
return ;
}
if (
type.startsWith('text/') ||
['txt', 'md', 'rtf', 'pdf'].includes(extension)
) {
return ;
}
if (
[
'html',
'css',
'js',
'jsx',
'ts',
'tsx',
'json',
'xml',
'php',
'py',
'rb',
'java',
'c',
'cpp',
'cs',
].includes(extension)
) {
return ;
}
if (['zip', 'rar', '7z', 'tar', 'gz', 'bz2'].includes(extension)) {
return ;
}
if (
['exe', 'msi', 'app', 'apk', 'deb', 'rpm'].includes(extension) ||
type.startsWith('application/')
) {
return ;
}
return ;
}
function useLazyRef(init: () => T): React.RefObject {
const ref = React.useRef(null);
if (ref.current === null) {
ref.current = init();
}
return ref as React.RefObject;
}
const StoreContext = React.createContext(null);
function useStoreContext(consumerName: string) {
const context = React.useContext(StoreContext);
if (!context) {
throw new Error(`\`${consumerName}\` must be used within \`${ROOT_NAME}\``);
}
return context;
}
function useStore(selector: (state: StoreState) => T): T {
const store = useStoreContext('useStore');
const lastValueRef = useLazyRef<{ value: T; state: StoreState } | null>(
() => null,
);
const getSnapshot = React.useCallback(() => {
const state = store.getState();
const prevValue = lastValueRef.current;
if (prevValue && prevValue.state === state) {
return prevValue.value;
}
const nextValue = selector(state);
lastValueRef.current = { value: nextValue, state };
return nextValue;
}, [store, selector, lastValueRef]);
return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);
}
const FileUploadContext = React.createContext(
null,
);
function useFileUploadContext(consumerName: string) {
const context = React.useContext(FileUploadContext);
if (!context) {
throw new Error(`\`${consumerName}\` must be used within \`${ROOT_NAME}\``);
}
return context;
}
export function FileUpload(props: FileUploadProps) {
const {
value,
defaultValue,
onValueChange,
onAccept,
onFileAccept,
onFileReject,
onFileValidate,
onUpload,
accept,
maxFiles,
maxSize,
dir: dirProp,
label,
name,
asChild,
disabled = false,
invalid = false,
multiple = false,
required = false,
children,
className,
...rootProps
} = props;
const inputId = React.useId();
const dropzoneId = React.useId();
const listId = React.useId();
const labelId = React.useId();
const dir = useDirectionPrimitive(dirProp);
const listeners = useLazyRef(() => new Set<() => void>()).current;
const files = useLazyRef