Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs(examples): added file upload example #1808

Merged
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion contributors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@
- mantey-github
- manzoorwanijk
- marcomafessolli
- marvinwu
- matchai
- matthew-burfield
- Matthew-Mallimo
Expand Down Expand Up @@ -225,4 +226,3 @@
- yesmeck
- yomeshgupta
- zachdtaylor
kentcdodds marked this conversation as resolved.
Show resolved Hide resolved
- zainfathoni
kentcdodds marked this conversation as resolved.
Show resolved Hide resolved
3 changes: 3 additions & 0 deletions examples/file-and-cloudinary-upload/.env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
CLOUD_NAME=
API_KEY=
API_SECRET=
5 changes: 5 additions & 0 deletions examples/file-and-cloudinary-upload/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules

/.cache
/build
/public/build
37 changes: 37 additions & 0 deletions examples/file-and-cloudinary-upload/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# upload file and images

This is a simple example of using the remix buildin [uploadHandler](https://remix.run/docs/en/v1/api/remix#uploadhandler) and Form with multipart data to upload an image file and display it,
it also show a simple(though not efficient way) of integrate with cloudinary without writing custom handler.

the relevent files are:

```
├── app
│ ├── routes
│ │ ├── cloudinary-upload.tsx // upload to cloudinary
│ │ └── local-upload.tsx // local upload using build in [createfileuploadhandler](https://remix.run/docs/en/v1/api/remix#unstable_createfileuploadhandler)
│ └── utils
│ └── utils.server.ts // init cloudinary nodejs client on server side
|── .env // holds cloudinary credentails
```

## steps to set up cloudinary

- sign up a free [cloudinary account](https://cloudinary.com/)
- get the cloudname, api key and api secret from dashboard
- copy the .env.sample to .env and populate the credentials


Open this example on [CodeSandbox](https://codesandbox.com):

[![Open in CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/remix-run/remix/tree/main/examples/file-and-cloudinary-upload)

## Example

## Related Links

### Remix Documentation

- [Handle Multiple Part Forms(File Uplodas)](https://remix.run/docs/en/v1/api/remix#unstable_parsemultipartformdata-node)
kentcdodds marked this conversation as resolved.
Show resolved Hide resolved
- [Upload Handler](https://remix.run/docs/en/v1/api/remix#uploadhandler)
- [Custom Uploader](https://remix.run/docs/en/v1/api/remix#custom-uploadhandler)
4 changes: 4 additions & 0 deletions examples/file-and-cloudinary-upload/app/entry.client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { hydrate } from "react-dom";
import { RemixBrowser } from "remix";

hydrate(<RemixBrowser />, document);
21 changes: 21 additions & 0 deletions examples/file-and-cloudinary-upload/app/entry.server.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { renderToString } from "react-dom/server";
import { RemixServer } from "remix";
import type { EntryContext } from "remix";

export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
const markup = renderToString(
<RemixServer context={remixContext} url={request.url} />
);

responseHeaders.set("Content-Type", "text/html");

return new Response("<!DOCTYPE html>" + markup, {
status: responseStatusCode,
headers: responseHeaders
});
}
27 changes: 27 additions & 0 deletions examples/file-and-cloudinary-upload/app/root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration
} from "remix";

export default function App() {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<Outlet />
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {
Form,
unstable_parseMultipartFormData,
useActionData,
json
} from "remix";
import type { ActionFunction, UploadHandler } from "remix";

import { uploadImage } from "~/utils/utils.server";

type ActionData = {
errorMsg?: string;
imgSrc?: string;
};

export const action: ActionFunction = async ({ request }) => {
let uploadHandler: UploadHandler = async ({ name, stream }) => {
if (name !== "img") {
stream.resume();
return;
}
const uploadedImage = await uploadImage(stream);
return uploadedImage.secure_url;
};

const formData = await unstable_parseMultipartFormData(
request,
uploadHandler
);
const imgSrc = formData.get("img");
if (!imgSrc) {
return json({
error: "something wrong"
});
}
return json({
imgSrc
});
};

export default function Index() {
const data = useActionData<ActionData>();
return (
<>
<Form method="post" encType="multipart/form-data">
<input type="file" name="img" accept="image/*" />
<button type="submit">upload to cloudinary</button>
</Form>
{data?.errorMsg && <h2>{data.errorMsg}</h2>}
{data?.imgSrc && (
<>
<h2>uploaded image</h2>
<img src={data.imgSrc} />
</>
)}
</>
);
}
52 changes: 52 additions & 0 deletions examples/file-and-cloudinary-upload/app/routes/local-upload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {
Form,
unstable_createFileUploadHandler,
unstable_parseMultipartFormData,
useActionData,
json
} from "remix";
import type { ActionFunction } from "remix";

type ActionData = {
errorMsg?: string;
imgSrc?: string;
};

export const action: ActionFunction = async ({ request }) => {
const uploadHandler = unstable_createFileUploadHandler({
directory: "public",
maxFileSize: 30000
});
const formData = await unstable_parseMultipartFormData(
request,
uploadHandler
);
const image = formData.get("img");
if (!image) {
return json({
error: "something wrong"
});
}
return json({
imgSrc: image.name
marvinwu marked this conversation as resolved.
Show resolved Hide resolved
});
};

export default function Index() {
const data = useActionData<ActionData>();
return (
<>
<Form method="post" encType="multipart/form-data">
<input type="file" name="img" accept="image/*" />
<button type="submit">upload image</button>
</Form>
{data?.errorMsg && <h2>{data.errorMsg}</h2>}
{data?.imgSrc && (
<>
<h2>uploaded image</h2>
<img alt="uploaded" src={data.imgSrc} />
</>
)}
</>
);
}
30 changes: 30 additions & 0 deletions examples/file-and-cloudinary-upload/app/utils/utils.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import dotenv from "dotenv";
import cloudinary from "cloudinary";
import type { Stream } from "stream";

dotenv.config();
Copy link
Contributor

@silvenon silvenon Feb 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love this example! Just a question about environment variables: according to Remix docs dotenv should only be used when starting the development script via node -r, see server environment variables. However, I prefer your way much better. Is there something wrong with it then?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I always avoid calling it via code. It's a potential source of bugs if you ship a ".env" to production by accident.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed! Fixed 👍

cloudinary.v2.config({
cloud_name: process.env.CLOUD_NAME,
api_key: process.env.API_KEY,
api_secret: process.env.API_SECRET
});

async function uploadImage(fileStream: Stream) {
return new Promise((resolve, reject) => {
const uploadStream = cloudinary.v2.uploader.upload_stream(
{
folder: "remix"
},
(error, result) => {
if (error) {
reject(error);
}
resolve(result);
}
);
fileStream.pipe(uploadStream);
});
}

console.log("configs", cloudinary.v2.config());
export { uploadImage };