feat(mui): add FileUpload kit primitive (hidden input + selected-file chips)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
117
src/admin/ui/FileUpload.tsx
Normal file
117
src/admin/ui/FileUpload.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
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 0–1 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 }}
|
||||
>
|
||||
×
|
||||
</IconButton>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -23,3 +23,4 @@ export { default as LoadingState } from "./LoadingState";
|
||||
export { default as FilterBar } from "./FilterBar";
|
||||
export { default as StatCard } from "./StatCard";
|
||||
export type { StatCardColor } from "./StatCard";
|
||||
export { default as FileUpload } from "./FileUpload";
|
||||
|
||||
Reference in New Issue
Block a user