Skip to content

Commit 8f9002e

Browse files
committed
chore(website): Added examples for useFileUpload
1 parent 49ce4d9 commit 8f9002e

File tree

18 files changed

+763
-8
lines changed

18 files changed

+763
-8
lines changed

packages/documentation/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"classnames": "^2.3.1",
5757
"codesandbox": "^2.2.3",
5858
"date-fns": "^2.22.1",
59+
"filesize": "^6.4.0",
5960
"formidable": "^1.2.2",
6061
"fuse.js": "6.4.6",
6162
"js-cookie": "^2.2.1",
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import React, { ReactElement } from "react";
2+
import filesize from "filesize";
3+
import {
4+
FileExtensionError,
5+
FileSizeError,
6+
isFileSizeError,
7+
isTooManyFilesError,
8+
TooManyFilesError,
9+
} from "@react-md/form";
10+
import { Text } from "@react-md/typography";
11+
12+
interface ErrorHeaderProps {
13+
error: TooManyFilesError | FileSizeError | FileExtensionError;
14+
}
15+
16+
export default function ErrorHeader({ error }: ErrorHeaderProps): ReactElement {
17+
if (isFileSizeError(error)) {
18+
const { type } = error;
19+
const limit = filesize(error.limit);
20+
if (type === "total") {
21+
return (
22+
<Text type="subtitle-2" margin="none">
23+
{`Unable to upload the following files due to total upload size limit (${limit})`}
24+
</Text>
25+
);
26+
}
27+
28+
const range = type === "min" ? "greater" : "less";
29+
return (
30+
<Text type="subtitle-2" margin="none">
31+
{`Unable to upload the following files because files must be ${range} than ${limit}`}
32+
</Text>
33+
);
34+
}
35+
if (isTooManyFilesError(error)) {
36+
const { limit } = error;
37+
return (
38+
<Text type="subtitle-2" margin="none">
39+
{`Unable to upload the following files due to total files allowed limit (${limit})`}
40+
</Text>
41+
);
42+
}
43+
44+
const { extensions } = error;
45+
return (
46+
<Text type="subtitle-2" margin="none">
47+
{`Invalid file extension. Must be one of ${extensions.join(", ")}`}
48+
</Text>
49+
);
50+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import React, { ReactElement, useRef, useState } from "react";
2+
import { Button } from "@react-md/button";
3+
import {
4+
Dialog,
5+
DialogContent,
6+
DialogFooter,
7+
DialogHeader,
8+
DialogTitle,
9+
} from "@react-md/dialog";
10+
import { FileValidationError } from "@react-md/form";
11+
12+
import ErrorRenderer from "./ErrorRenderer";
13+
14+
export interface ErrorModalProps {
15+
errors: readonly FileValidationError<never>[];
16+
clearErrors(): void;
17+
}
18+
19+
export default function ErrorModal({
20+
errors,
21+
clearErrors,
22+
}: ErrorModalProps): ReactElement {
23+
const [visible, setVisible] = useState(false);
24+
const prevErrors = useRef(errors);
25+
26+
// why?
27+
// makes it so the errors don't disappear during the exit animation
28+
if (errors !== prevErrors.current) {
29+
prevErrors.current = errors;
30+
if (!visible && errors.length) {
31+
setVisible(true);
32+
}
33+
}
34+
35+
const onRequestClose = (): void => {
36+
setVisible(false);
37+
};
38+
39+
return (
40+
<Dialog
41+
id="error-modal"
42+
aria-labelledby="error-modal-title"
43+
modal
44+
onRequestClose={onRequestClose}
45+
visible={visible}
46+
onExited={clearErrors}
47+
>
48+
<DialogHeader>
49+
<DialogTitle id="error-modal-title">File Upload Errors</DialogTitle>
50+
</DialogHeader>
51+
<DialogContent>
52+
{errors.map((error) => (
53+
<ErrorRenderer key={error.key} error={error} />
54+
))}
55+
</DialogContent>
56+
<DialogFooter>
57+
<Button onClick={onRequestClose}>Okay</Button>
58+
</DialogFooter>
59+
</Dialog>
60+
);
61+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import React, { Fragment, ReactElement } from "react";
2+
import filesize from "filesize";
3+
import { FileValidationError, isFileSizeError } from "@react-md/form";
4+
import { List, SimpleListItem } from "@react-md/list";
5+
import { Text } from "@react-md/typography";
6+
7+
import ErrorHeader from "./ErrorHeader";
8+
9+
export interface ErrorRendererProps {
10+
error: FileValidationError<never>;
11+
}
12+
13+
export default function ErrorRenderer({
14+
error,
15+
}: ErrorRendererProps): ReactElement {
16+
if ("files" in error) {
17+
const { key, files } = error;
18+
return (
19+
<Fragment key={key}>
20+
<ErrorHeader error={error} />
21+
<List>
22+
{files.map((file, i) => (
23+
<SimpleListItem
24+
key={i}
25+
primaryText={file.name}
26+
secondaryText={isFileSizeError(error) && filesize(file.size)}
27+
/>
28+
))}
29+
</List>
30+
</Fragment>
31+
);
32+
}
33+
34+
// error
35+
/* ^ is a {@link FileAccessError} */
36+
return (
37+
<Text margin="none">
38+
File access is restricted. Try a different file or folder.
39+
</Text>
40+
);
41+
}

packages/documentation/src/components/Demos/Form/FileInputs/FileInputs.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,29 @@ import { DemoConfig } from "components/Demos/types";
77
import FileInputExample from "./FileInputExample";
88
import fileInputExample from "./FileInputExample.md";
99

10+
import SimpleFileUpload from "./SimpleFileUpload";
11+
import simpleFileUpload from "./SimpleFileUpload.md";
12+
13+
import ServerUploadExample from "./ServerUploadExample";
14+
import serverUploadExample from "./ServerUploadExample.md";
15+
1016
const demos: DemoConfig[] = [
1117
{
1218
name: "File Input Example",
1319
description: fileInputExample,
1420
children: <FileInputExample />,
1521
},
22+
{
23+
name: "Simple File Upload",
24+
description: simpleFileUpload,
25+
children: <SimpleFileUpload />,
26+
},
27+
{
28+
name: "Server Upload Example",
29+
description: serverUploadExample,
30+
children: <ServerUploadExample />,
31+
disableCard: true,
32+
},
1633
];
1734

1835
export default function FileInputs(): ReactElement {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
@import '~@react-md/icon/dist/mixins';
2+
3+
.responsive {
4+
max-height: 100%;
5+
max-width: 100%;
6+
}
7+
8+
.overlay {
9+
@include rmd-icon-theme-update-var(size, 2rem);
10+
@include rmd-icon-theme-update-var(color, $rmd-theme-error);
11+
12+
align-items: center;
13+
display: flex;
14+
flex-direction: column;
15+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import React, { ReactElement, useState } from "react";
2+
import { FileReaderResult, isImageFile, isVideoFile } from "@react-md/form";
3+
import { ErrorSVGIcon } from "@react-md/material-icons";
4+
import { MediaOverlay } from "@react-md/media";
5+
import { TextIconSpacing } from "@react-md/icon";
6+
import { Text } from "@react-md/typography";
7+
8+
import styles from "./Preview.module.scss";
9+
10+
const THREE_MB = 3 * 1024 * 1024;
11+
12+
export interface PreviewProps {
13+
file: File;
14+
result: FileReaderResult;
15+
}
16+
17+
export default function Preview({ file, result }: PreviewProps): ReactElement {
18+
const [error, setError] = useState(false);
19+
20+
const onError = (): void => setError(true);
21+
const isGifLike = file.size < THREE_MB;
22+
23+
return (
24+
<>
25+
{(typeof result !== "string" || error) && (
26+
<MediaOverlay position="middle" className={styles.overlay}>
27+
<TextIconSpacing stacked icon={<ErrorSVGIcon />}>
28+
<Text>
29+
{!error
30+
? "I did not set up a preview for this file type."
31+
: "Your Browser is unable to preview this file."}
32+
</Text>
33+
</TextIconSpacing>
34+
</MediaOverlay>
35+
)}
36+
{typeof result === "string" && (
37+
<>
38+
{isImageFile(file) && (
39+
<img
40+
src={result}
41+
alt=""
42+
onError={onError}
43+
className={styles.responsive}
44+
/>
45+
)}
46+
{isVideoFile(file) && (
47+
<video
48+
muted
49+
controls={!isGifLike}
50+
loop={isGifLike}
51+
autoPlay={isGifLike}
52+
onError={onError}
53+
className={styles.responsive}
54+
>
55+
<source src={result} type={file.type || "video/webm"} />
56+
</video>
57+
)}
58+
</>
59+
)}
60+
</>
61+
);
62+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
@import '~@react-md/card/dist/mixins';
2+
@import '~@react-md/icon/dist/mixins';
3+
@import '~@react-md/progress/dist/mixins';
4+
5+
.container {
6+
display: flex;
7+
flex-direction: column;
8+
}
9+
10+
.content {
11+
align-items: center;
12+
flex: 1 1 auto;
13+
justify-content: center;
14+
min-height: 0;
15+
position: relative;
16+
}
17+
18+
.icon {
19+
@include rmd-icon-theme-update-var(size, 4rem);
20+
21+
margin-top: 10%;
22+
}
23+
24+
.progress {
25+
@include rmd-progress-theme-update-var(linear-size, 0.5rem);
26+
27+
margin-top: auto;
28+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import React, { HTMLAttributes, ReactElement } from "react";
2+
import cn from "classnames";
3+
import filesize from "filesize";
4+
import { Button } from "@react-md/button";
5+
import {
6+
Card,
7+
CardContent,
8+
CardHeader,
9+
CardSubtitle,
10+
CardTitle,
11+
} from "@react-md/card";
12+
import {
13+
FileReaderResult,
14+
FileUploadActions,
15+
FileUploadStats,
16+
} from "@react-md/form";
17+
import { CloseSVGIcon } from "@react-md/material-icons";
18+
import { LinearProgress } from "@react-md/progress";
19+
import { GridListCell } from "@react-md/utils";
20+
21+
import FileSVGIcon from "icons/FileSVGIcon";
22+
23+
import styles from "./PreviewFile.module.scss";
24+
import Preview from "./Preview";
25+
26+
export interface PreviewFileProps
27+
extends Omit<FileUploadStats, "key">,
28+
HTMLAttributes<HTMLDivElement> {
29+
id: string;
30+
fileKey: string;
31+
result?: FileReaderResult;
32+
remove: FileUploadActions["remove"];
33+
}
34+
35+
export default function PreviewFile({
36+
id,
37+
file,
38+
fileKey,
39+
result,
40+
status,
41+
progress,
42+
remove,
43+
className,
44+
...props
45+
}: PreviewFileProps): ReactElement {
46+
const { name, size } = file;
47+
return (
48+
<GridListCell clone square>
49+
<Card {...props} id={id} className={cn(styles.container, className)}>
50+
<CardHeader
51+
afterChildren={
52+
<Button
53+
id={`${id}-remove`}
54+
aria-label="Remove"
55+
buttonType="icon"
56+
onClick={() => remove(fileKey)}
57+
>
58+
<CloseSVGIcon />
59+
</Button>
60+
}
61+
>
62+
<CardTitle small noWrap>
63+
{name}
64+
</CardTitle>
65+
<CardSubtitle>{filesize(size)}</CardSubtitle>
66+
</CardHeader>
67+
<CardContent className={cn(styles.container, styles.content)}>
68+
{status !== "complete" && (
69+
<>
70+
<FileSVGIcon className={styles.icon} />
71+
<LinearProgress
72+
id={`${id}-upload-progress`}
73+
value={progress}
74+
className={styles.progress}
75+
/>
76+
</>
77+
)}
78+
{status === "complete" && (
79+
<Preview file={file} result={result ?? null} />
80+
)}
81+
</CardContent>
82+
</Card>
83+
</GridListCell>
84+
);
85+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
This example is a bit more complex than the previous and showcases:
2+
3+
- adding additional validation around what types of files can be uploaded
4+
- enabling drag and drop uploads with `useDropzone`
5+
- uploading files to a server using
6+
[XHLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest)
7+
- using the returned value from the hook to display additional information to
8+
the user
9+
10+
> Note: The server upload portion will not work once converted imported into a
11+
> code sandbox.

0 commit comments

Comments
 (0)