Whether it’s for sharing your knowledge or giving the world a chance to live vicariously through your experiences, vlogs have provided a more immersive and engaging medium for sharing content and unleashing the creativity of content providers.
In this article, I will show you how to build a simple vlog using React. The application will have an index view displaying all the videos and another for uploading a new video.
We will take advantage of Cloudinary’s comprehensive API and easy-to-use transformation URLs to manage media resources. Not only will we be able to store resources via Cloudinary, but uploaded videos can also be automatically transcoded to all relevant formats suitable for web viewing, optimized for web browsers and mobile devices, and transformed in real-time. This makes for a seamless user experience with minimal coding. You will need a Cloudinary account for this tutorial. Create one here if you don’t already have one.
For UI components in our application, we will use antd while axios will be used for uploading our videos to Cloudinary.
Here’s a link to the demo on CodeSandbox.
Create a new React app using the following command:
npx create-react-app cloudinary_vlog_demo
Next, add the project dependencies using the following command:
npm install antd @ant-design/icons cloudinary-react axios react-router-dom@6
Code language: CSS (css)
We need to import the antd CSS. To do this, open your src/App.css
file and the following to it:
@import '~antd/dist/antd.css';
Code language: CSS (css)
Next, we need to create some environment variables to hold our Cloudinary details. We will be sending images to Cloudinary via unsigned POST requests for this tutorial. To do this, we need our account cloud name and an unsigned upload preset. Create a new file called .env
at the root of the project and add the following to it:
REACT_APP_CLOUD_NAME = 'INSERT YOUR CLOUD NAME HERE'
REACT_APP_UPLOAD_PRESET = 'INSERT YOUR UNSIGNED UPLOAD PRESET KEY HERE'
Code language: JavaScript (javascript)
This will be used as a default when the project is set up on another system. To update your local environment, create a copy of the .env
file using the following command.
cp .env .env.local
Code language: CSS (css)
By default, this local file is added to .gitignore
and mitigates the security risk of inadvertently exposing secret credentials to the public. You can update your .env.local
file with your Cloudinary cloud name and generated upload preset.
Create a new folder named util
in the src' directory. This folder will hold all the helper classes we will need in our components. In the
utilfolder, create a file called
cloudinaryConfig.js. This file will give access to the environment variables and prevent repeated
process.env.calls throughout the project. Add the following to the
cloudinaryConfig.js` file:
export const cloudName = process.env.REACT_APP_CLOUD_NAME;
export const uploadPreset = process.env.REACT_APP_UPLOAD_PRESET;
export const uploadTag = "my_cloudinary_vlog";
Code language: JavaScript (javascript)
In addition to accessing the environment variables, we also declare a constant named uploadTag
. This will be attached to the videos on upload and used to retrieve the vlog posts from Cloudinary.
We need to make two API requests for this application: uploading a new video and retrieving the list of uploaded videos. Create a new file named api.js
in the util
folder and add the following to it:
import axios from "axios";
import {cloudName, uploadPreset, uploadTag} from "./cloudinaryConfig";
export const getVideos = ({successCallback}) => {
axios
.get(`https://res.cloudinary.com/${cloudName}/video/list/${uploadTag}.json`)
.then((response) => successCallback(response.data.resources));
};
const formatMetadata = metadata => {
return Object.entries(metadata)
.reduce(
(result, [key, value]) => `${result}${result !== '' ? '|' : ''}${key}=${value}`
, ''
);
}
export const uploadVideo = ({file, metadata, successCallback}) => {
const url = `https://api.cloudinary.com/v1_1/${cloudName}/video/upload`;
const data = new FormData();
data.append("file", file);
data.append("upload_preset", uploadPreset);
data.append("context", formatMetadata(metadata));
data.append("tags", uploadTag);
axios
.post(url, data, {
headers: {
"Content-Type": "multipart/form-data",
},
})
.then(response => successCallback(response.data))
}
Code language: JavaScript (javascript)
In this file, we export two functions – getVideos
and uploadVideo
. The getVideos
function uses axios to make a GET
request to Cloudinary. The resources in the JSON response are passed as a parameter to the success callback function, which allows the component perform some action once the list of resources becomes available. This is made possible by Cloudinary’s Client-side asset lists feature.
To ensure that this feature is available on your Cloudinary account, you must ensure that the Resource list option is enabled. By default, the list delivery type is restricted. To enable it, open the Security settings in your Management console, and clear the Resource list item under Restricted image types. You may want to remove this option only temporarily, as needed. Alternatively, you can bypass this (and any) delivery type restriction using a signed URL.
The uploadVideo
function takes a file, some collected metadata on the video, and a callback function to be executed when the request is handled properly. Using the formatMetadata
function, the metadata
object is converted to the expected contextual metadata format for file uploads to Cloudinary. Essentially, an =
character separates a key from its value while a |
character separates key-value pairs. The file, upload preset, context metadata, and upload tag are passed in the formData request to Cloudinary. The provided success callback is used to trigger further action in the component once a successful response is received.
In the src
folder, create a folder called components
. In the src/components
folder, create a file called VideoUpload.js
and add the following code to it:
import {useState} from "react";
import {Button, Card, Col, Form, Input, message, Upload} from "antd";
import {uploadVideo} from "../util/api";
import {UploadOutlined} from "@ant-design/icons";
import {Navigate} from "react-router-dom";
const VideoUpload = () => {
const [isUploading, setIsUploading] = useState(false);
const [uploadedFile, setUploadedFile] = useState(null);
const [shouldRedirect, setShouldRedirect] = useState(false);
const formItemLayout = {
labelCol: {
sm: {span: 4},
},
wrapperCol: {
sm: {span: 18},
},
};
const onFinish = (values) => {
if (uploadedFile === null) {
message.error('You need to upload a video first');
} else {
setIsUploading(true);
uploadVideo({
file: uploadedFile,
metadata: values,
successCallback: () => {
setIsUploading(false);
setShouldRedirect(true);
}
});
}
};
const onFailedSubmission = (errorInfo) => {
console.log("Failed:", errorInfo);
};
const props = {
name: "file",
onRemove: () => {
setUploadedFile(null);
},
beforeUpload: (file) => {
setUploadedFile(file);
return false;
},
showUploadList: false,
maxCount: 1,
};
return (shouldRedirect ? <Navigate to='/'/> :
<Card style={{margin: "auto", width: "50%"}}>
<Form
{...formItemLayout}
onFinish={onFinish}
onFinishFailed={onFailedSubmission}
autoComplete="off"
>
<Form.Item
label="Title"
name="title"
rules={[{
required: true,
message: "Please provide a title for the video",
}]}
>
<Input/>
</Form.Item>
<Form.Item
name="description"
label="Description"
rules={[{
required: true,
message: 'Please provide a brief summary of the video'
}]}
>
<Input.TextArea showCount maxLength={1000}/>
</Form.Item>
<Col span={8} offset={9} style={{marginBottom: '10px'}}>
<Upload {...props}>
<Button icon={<UploadOutlined/>} loading={isUploading}>
Click to Upload Video
</Button>
</Upload>
</Col>
<Form.Item wrapperCol={{offset: 5, span: 16}}>
<Button
type="primary"
htmlType="submit"
loading={isUploading}
>
Submit
</Button>
</Form.Item>
</Form>
</Card>
);
};
export default VideoUpload;
Code language: JavaScript (javascript)
This component renders the form that will be used to upload videos. In addition to the video file, we also make provision for the user to provide the title of the post and a brief description. For the sake of this tutorial, the post title and description are required.
The onFinish
function is called when the user clicks the Submit button after filling the required fields. The function checks to ensure that a file has been uploaded before making a POST request using the uploadVideo
we declared earlier. The function passed as a callback on success sets the value shouldRedirect
to true. This, in turn, triggers the rendering of the Navigate component provided by React Router, which redirects the user to the index page.
By returning false
in the beforeUpload
key of props, we disable the default behavior of the Upload component, which is to upload the selected file to a provided URL. We also restrict the maximum number of files to 1.
In the src/components
folder, create a file called VideoGrid.js
and add the following to it:
import React, {useEffect, useState} from "react";
import {getVideos} from "../util/api";
import {Card, Col, Row} from "antd";
import {Video} from 'cloudinary-react';
import {cloudName} from "../util/cloudinaryConfig";
const VideoGrid = () => {
const [videos, setVideos] = useState([]);
useEffect(() => {
getVideos({
successCallback: setVideos
})
}, []);
return <>
<h1>Cloudinary Powered Vlog</h1>
<Row
gutter={[16, 16]}
justify='center'
align='middle'
>
{videos.map(video => {
const {title, description} = video.context.custom;
const {public_id: publicId} = video;
return <Col key={publicId}>
<Card style={{width: '600px'}} cover={
<Video
cloudName={cloudName}
publicId={publicId}
controls={true}
width='480'/>
}>
<Card.Meta title={title} description={description}/>
</Card>
</Col>
})}
</Row>
</>;
};
export default VideoGrid;
Code language: JavaScript (javascript)
On render, we use an effect to retrieve the list of videos using the getVideos
function we declared earlier. The returned resources are saved to the videos
state variable. Each video is rendered using the Video
component provided by cloudinary-react
with the title and description rendered underneath.
In the src/components
folder, create a file named Menu.js
and add the following to it:
import {Menu as AntDMenu} from 'antd';
import {useState} from "react";
import {Link} from "react-router-dom";
import {HomeOutlined, UploadOutlined} from "@ant-design/icons";
const Menu = () => {
const [currentlySelected, setCurrentlySelected] = useState('home');
const handleMenuSelection = e => {
setCurrentlySelected(e.key);
}
return (
<AntDMenu
mode='horizontal'
onClick={handleMenuSelection}
selectedKeys={[currentlySelected]}
>
<AntDMenu.Item key='home' icon={<HomeOutlined/>}>
<Link to='/'>Home</Link>
</AntDMenu.Item>
<AntDMenu.Item key='upload' icon={<UploadOutlined/>}>
<Link to='/upload'>Upload Video</Link>
</AntDMenu.Item>
</AntDMenu>
);
};
export default Menu;
Code language: JavaScript (javascript)
Using the AntD Menu component, we provide a means of navigation between the home page (/) and the video upload page (/upload).
Now that we have all our components in place let’s implement the routing functionality to allow us navigate between components. In the src/util
folder, create a file called routes.js
and add the following to it:
import VideoGrid from "../components/VideoGrid";
import VideoUpload from "../components/VideoUpload";
export default [
{
path: '/',
element: <VideoGrid/>
},
{
path: '/upload',
element: <VideoUpload/>
}
];
Code language: JavaScript (javascript)
Here we define Route Objects, which will be used by the useRoutes
hook to render the <Route>
component.
Next, update src/App.js
to match the following code:
import './App.css';
import {useRoutes} from "react-router-dom";
import routes from "./util/routes";
import Menu from "./components/Menu";
import {Col, Row} from "antd";
function App() {
const router = useRoutes(routes);
return <div style={{margin: "1%"}}>
<Menu/>
<div style={{textAlign: 'center'}}>
<Row
justify='center'
align='middle'
style={{textAlign: 'center'}}
>
<Col style={{width: '100%', margin: '2%'}}>
{router}
</Col>
</Row>
</div>
</div>
}
export default App;
Code language: JavaScript (javascript)
Using the earlier declared routes
object and the useRoutes
hook, we render a <Route>
component.
Finally, wrap the <App>
component in a BrowserRouter
. To do this, update src/index.js
to match the following:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import {BrowserRouter} from "react-router-dom";
ReactDOM.render(
<React.StrictMode>
<BrowserRouter>
<App/>
</BrowserRouter>
</React.StrictMode>,
document.getElementById('root')
);
Code language: JavaScript (javascript)
With this in place, our application is complete! Run your application using this command:
npm start
By default, the application will be available at http://localhost:3000/. The final result will look like the gif shown below.
Find the complete project here on GitHub.
In this article, we looked at how Cloudinary can be used to manage the media content for a vlog. One of the major advantages of using Cloudinary for delivering video content is that uploaded videos can be automatically transcoded to all relevant formats suitable for web viewing, optimized for web browsers and mobile devices, and transformed in real-time to fit your graphic design.
Resources you may find helpful: