Files
app/src/admin/ui/FileUpload.tsx
2026-06-07 00:02:22 +02:00

118 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useRef, type ChangeEvent } from "react";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import IconButton from "@mui/material/IconButton";
import Button from "./Button";
function formatSize(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} kB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
export interface FileUploadProps {
/** Selected files. Single-select mode keeps 01 items. */
files: File[];
onFilesChange: (files: File[]) => void;
/** e.g. "application/pdf" */
accept?: string;
/** Allow multiple files (appends on pick). Default false = single (replaces). */
multiple?: boolean;
/** Picker button label. */
buttonLabel?: string;
disabled?: boolean;
}
/**
* File picker: a button wrapping a hidden `<input type=file>`, with selected-file
* chips (name + size + remove). Single mode (default) replaces the selection;
* `multiple` appends. Value is always `File[]` so callers handle both uniformly.
* Validation (size/count/type beyond `accept`) stays with the caller.
*/
export default function FileUpload({
files,
onFilesChange,
accept,
multiple = false,
buttonLabel = "Vybrat soubor",
disabled,
}: FileUploadProps) {
const inputRef = useRef<HTMLInputElement>(null);
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const picked = Array.from(e.target.files ?? []);
if (picked.length === 0) return;
onFilesChange(multiple ? [...files, ...picked] : picked.slice(0, 1));
// Reset so picking the same file again (after removal) still fires onChange.
if (inputRef.current) inputRef.current.value = "";
};
const remove = (idx: number) =>
onFilesChange(files.filter((_, i) => i !== idx));
return (
<Box>
<Button
component="label"
variant="outlined"
color="inherit"
size="small"
disabled={disabled}
>
{buttonLabel}
<input
ref={inputRef}
type="file"
hidden
accept={accept}
multiple={multiple}
onChange={handleChange}
/>
</Button>
{files.length > 0 && (
<Box sx={{ mt: 1, display: "flex", flexWrap: "wrap", gap: 1 }}>
{files.map((f, i) => (
<Box
key={`${f.name}-${f.size}-${i}`}
sx={{
display: "flex",
alignItems: "center",
gap: 0.5,
pl: 1,
pr: 0.25,
py: 0.25,
borderRadius: 1,
bgcolor: "action.hover",
}}
>
<Typography
variant="caption"
sx={{
maxWidth: 240,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{f.name}
</Typography>
<Typography variant="caption" color="text.secondary">
({formatSize(f.size)})
</Typography>
<IconButton
size="small"
onClick={() => remove(i)}
aria-label="Odebrat soubor"
disabled={disabled}
sx={{ p: 0.25, fontSize: "1rem", lineHeight: 1 }}
>
&times;
</IconButton>
</Box>
))}
</Box>
)}
</Box>
);
}