Skip to content

Commit

Permalink
chore(website): Added examples for useFileUpload
Browse files Browse the repository at this point in the history
  • Loading branch information
mlaursen committed Jul 18, 2021
1 parent 49ce4d9 commit 8f9002e
Show file tree
Hide file tree
Showing 18 changed files with 763 additions and 8 deletions.
1 change: 1 addition & 0 deletions packages/documentation/package.json
Expand Up @@ -56,6 +56,7 @@
"classnames": "^2.3.1",
"codesandbox": "^2.2.3",
"date-fns": "^2.22.1",
"filesize": "^6.4.0",
"formidable": "^1.2.2",
"fuse.js": "6.4.6",
"js-cookie": "^2.2.1",
Expand Down
@@ -0,0 +1,50 @@
import React, { ReactElement } from "react";
import filesize from "filesize";
import {
FileExtensionError,
FileSizeError,
isFileSizeError,
isTooManyFilesError,
TooManyFilesError,
} from "@react-md/form";
import { Text } from "@react-md/typography";

interface ErrorHeaderProps {
error: TooManyFilesError | FileSizeError | FileExtensionError;
}

export default function ErrorHeader({ error }: ErrorHeaderProps): ReactElement {
if (isFileSizeError(error)) {
const { type } = error;
const limit = filesize(error.limit);
if (type === "total") {
return (
<Text type="subtitle-2" margin="none">
{`Unable to upload the following files due to total upload size limit (${limit})`}
</Text>
);
}

const range = type === "min" ? "greater" : "less";
return (
<Text type="subtitle-2" margin="none">
{`Unable to upload the following files because files must be ${range} than ${limit}`}
</Text>
);
}
if (isTooManyFilesError(error)) {
const { limit } = error;
return (
<Text type="subtitle-2" margin="none">
{`Unable to upload the following files due to total files allowed limit (${limit})`}
</Text>
);
}

const { extensions } = error;
return (
<Text type="subtitle-2" margin="none">
{`Invalid file extension. Must be one of ${extensions.join(", ")}`}
</Text>
);
}
@@ -0,0 +1,61 @@
import React, { ReactElement, useRef, useState } from "react";
import { Button } from "@react-md/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@react-md/dialog";
import { FileValidationError } from "@react-md/form";

import ErrorRenderer from "./ErrorRenderer";

export interface ErrorModalProps {
errors: readonly FileValidationError<never>[];
clearErrors(): void;
}

export default function ErrorModal({
errors,
clearErrors,
}: ErrorModalProps): ReactElement {
const [visible, setVisible] = useState(false);
const prevErrors = useRef(errors);

// why?
// makes it so the errors don't disappear during the exit animation
if (errors !== prevErrors.current) {
prevErrors.current = errors;
if (!visible && errors.length) {
setVisible(true);
}
}

const onRequestClose = (): void => {
setVisible(false);
};

return (
<Dialog
id="error-modal"
aria-labelledby="error-modal-title"
modal
onRequestClose={onRequestClose}
visible={visible}
onExited={clearErrors}
>
<DialogHeader>
<DialogTitle id="error-modal-title">File Upload Errors</DialogTitle>
</DialogHeader>
<DialogContent>
{errors.map((error) => (
<ErrorRenderer key={error.key} error={error} />
))}
</DialogContent>
<DialogFooter>
<Button onClick={onRequestClose}>Okay</Button>
</DialogFooter>
</Dialog>
);
}
@@ -0,0 +1,41 @@
import React, { Fragment, ReactElement } from "react";
import filesize from "filesize";
import { FileValidationError, isFileSizeError } from "@react-md/form";
import { List, SimpleListItem } from "@react-md/list";
import { Text } from "@react-md/typography";

import ErrorHeader from "./ErrorHeader";

export interface ErrorRendererProps {
error: FileValidationError<never>;
}

export default function ErrorRenderer({
error,
}: ErrorRendererProps): ReactElement {
if ("files" in error) {
const { key, files } = error;
return (
<Fragment key={key}>
<ErrorHeader error={error} />
<List>
{files.map((file, i) => (
<SimpleListItem
key={i}
primaryText={file.name}
secondaryText={isFileSizeError(error) && filesize(file.size)}
/>
))}
</List>
</Fragment>
);
}

// error
/* ^ is a {@link FileAccessError} */
return (
<Text margin="none">
File access is restricted. Try a different file or folder.
</Text>
);
}
Expand Up @@ -7,12 +7,29 @@ import { DemoConfig } from "components/Demos/types";
import FileInputExample from "./FileInputExample";
import fileInputExample from "./FileInputExample.md";

import SimpleFileUpload from "./SimpleFileUpload";
import simpleFileUpload from "./SimpleFileUpload.md";

import ServerUploadExample from "./ServerUploadExample";
import serverUploadExample from "./ServerUploadExample.md";

const demos: DemoConfig[] = [
{
name: "File Input Example",
description: fileInputExample,
children: <FileInputExample />,
},
{
name: "Simple File Upload",
description: simpleFileUpload,
children: <SimpleFileUpload />,
},
{
name: "Server Upload Example",
description: serverUploadExample,
children: <ServerUploadExample />,
disableCard: true,
},
];

export default function FileInputs(): ReactElement {
Expand Down
@@ -0,0 +1,15 @@
@import '~@react-md/icon/dist/mixins';

.responsive {
max-height: 100%;
max-width: 100%;
}

.overlay {
@include rmd-icon-theme-update-var(size, 2rem);
@include rmd-icon-theme-update-var(color, $rmd-theme-error);

align-items: center;
display: flex;
flex-direction: column;
}
@@ -0,0 +1,62 @@
import React, { ReactElement, useState } from "react";
import { FileReaderResult, isImageFile, isVideoFile } from "@react-md/form";
import { ErrorSVGIcon } from "@react-md/material-icons";
import { MediaOverlay } from "@react-md/media";
import { TextIconSpacing } from "@react-md/icon";
import { Text } from "@react-md/typography";

import styles from "./Preview.module.scss";

const THREE_MB = 3 * 1024 * 1024;

export interface PreviewProps {
file: File;
result: FileReaderResult;
}

export default function Preview({ file, result }: PreviewProps): ReactElement {
const [error, setError] = useState(false);

const onError = (): void => setError(true);
const isGifLike = file.size < THREE_MB;

return (
<>
{(typeof result !== "string" || error) && (
<MediaOverlay position="middle" className={styles.overlay}>
<TextIconSpacing stacked icon={<ErrorSVGIcon />}>
<Text>
{!error
? "I did not set up a preview for this file type."
: "Your Browser is unable to preview this file."}
</Text>
</TextIconSpacing>
</MediaOverlay>
)}
{typeof result === "string" && (
<>
{isImageFile(file) && (
<img
src={result}
alt=""
onError={onError}
className={styles.responsive}
/>
)}
{isVideoFile(file) && (
<video
muted
controls={!isGifLike}
loop={isGifLike}
autoPlay={isGifLike}
onError={onError}
className={styles.responsive}
>
<source src={result} type={file.type || "video/webm"} />
</video>
)}
</>
)}
</>
);
}
@@ -0,0 +1,28 @@
@import '~@react-md/card/dist/mixins';
@import '~@react-md/icon/dist/mixins';
@import '~@react-md/progress/dist/mixins';

.container {
display: flex;
flex-direction: column;
}

.content {
align-items: center;
flex: 1 1 auto;
justify-content: center;
min-height: 0;
position: relative;
}

.icon {
@include rmd-icon-theme-update-var(size, 4rem);

margin-top: 10%;
}

.progress {
@include rmd-progress-theme-update-var(linear-size, 0.5rem);

margin-top: auto;
}
@@ -0,0 +1,85 @@
import React, { HTMLAttributes, ReactElement } from "react";
import cn from "classnames";
import filesize from "filesize";
import { Button } from "@react-md/button";
import {
Card,
CardContent,
CardHeader,
CardSubtitle,
CardTitle,
} from "@react-md/card";
import {
FileReaderResult,
FileUploadActions,
FileUploadStats,
} from "@react-md/form";
import { CloseSVGIcon } from "@react-md/material-icons";
import { LinearProgress } from "@react-md/progress";
import { GridListCell } from "@react-md/utils";

import FileSVGIcon from "icons/FileSVGIcon";

import styles from "./PreviewFile.module.scss";
import Preview from "./Preview";

export interface PreviewFileProps
extends Omit<FileUploadStats, "key">,
HTMLAttributes<HTMLDivElement> {
id: string;
fileKey: string;
result?: FileReaderResult;
remove: FileUploadActions["remove"];
}

export default function PreviewFile({
id,
file,
fileKey,
result,
status,
progress,
remove,
className,
...props
}: PreviewFileProps): ReactElement {
const { name, size } = file;
return (
<GridListCell clone square>
<Card {...props} id={id} className={cn(styles.container, className)}>
<CardHeader
afterChildren={
<Button
id={`${id}-remove`}
aria-label="Remove"
buttonType="icon"
onClick={() => remove(fileKey)}
>
<CloseSVGIcon />
</Button>
}
>
<CardTitle small noWrap>
{name}
</CardTitle>
<CardSubtitle>{filesize(size)}</CardSubtitle>
</CardHeader>
<CardContent className={cn(styles.container, styles.content)}>
{status !== "complete" && (
<>
<FileSVGIcon className={styles.icon} />
<LinearProgress
id={`${id}-upload-progress`}
value={progress}
className={styles.progress}
/>
</>
)}
{status === "complete" && (
<Preview file={file} result={result ?? null} />
)}
</CardContent>
</Card>
</GridListCell>
);
}
@@ -0,0 +1,11 @@
This example is a bit more complex than the previous and showcases:

- adding additional validation around what types of files can be uploaded
- enabling drag and drop uploads with `useDropzone`
- uploading files to a server using
[XHLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest)
- using the returned value from the hook to display additional information to
the user

> Note: The server upload portion will not work once converted imported into a
> code sandbox.

0 comments on commit 8f9002e

Please sign in to comment.