In this Media Jam, we will discuss how to build an Image Gallery in Sapper with Cloudinary and upload images to Cloudinary with drag and drop operation.
If you want to jump right into the code, check out the GitHub Repo here.
Sandbox
Before we get started, I wanted to point out that you can play around with the code described in this Jam on CodeSandbox, and you can find the live version here.
How To Setup and Install a Sapper Project
In your project's root directory, run the following commands in the terminal to quickly set up the initial Sapper project using the sapper-template
.
1npx degit "sveltejs/sapper-template#webpack" sapper-cloudinary-example2cd sapper-cloudinary-example3npm install4npm run dev
The last command will start the Sapper development server on http://localhost:3000/.
Head over to your Cloudinary dashboard and copy the API keys.
Create a new file named .env
in your project's root directory and paste the API keys in it, as shown below.
1CLOUDINARY_CLOUD=''2CLOUDINARY_KEY= ''3CLOUDINARY_SECRET=''
To use these environment variables, you will also need to install the dotenv
package.
Run the following command to install dotenv
and cloudinary
libraries where cloudinary
is the Node.js/server-side SDK you will use to upload the images to Cloudinary.
1npm i dotenv cloudinary
How To Display Images on the App
The first step is to build the image gallery where you will display all the uploaded images. Create a new file named index.json.js
inside the src
directory, where you will create the server route to fetch the images from Cloudinary.
Server routes in Sapper are used to create JSON API, which exports functions, having HTTP request
and response
objects as arguments, corresponding to HTTP methods like GET
, POST
, etc.
You can create both the GET
method to fetch the images and the POST
method to upload the image inside the same index.json.js
file, and Sapper will differentiate between them based on the type of request made.
Run the following command to create the index.json.js
file.
1touch src/routes/index.json.js
Add the following code to index.json.js
.
1require("dotenv").config();2const cloudinary = require("cloudinary").v2;34cloudinary.config({5 cloud_name: process.env.CLOUDINARY_CLOUD,6 api_key: process.env.CLOUDINARY_KEY,7 api_secret: process.env.CLOUDINARY_SECRET,8});910export function get(req, res) {11 res.writeHead(200, {12 "Content-Type": "application/json",13 });1415 let secureUrls;1617 cloudinary.api.resources({type: 'upload'}, function (error, result) {18 secureUrls = JSON.stringify(19 result.resources.map((pic) => {20 return { secureUrl: pic.secure_url };21 })22 );2324 res.end(secureUrls);25 });26}
In the above code, you first access the environment variables using require("dotenv").config();
and pass them to cloudinary
. Then, you create and export the get
function where the images are fetched from Cloudinary using Cloudinary's Admin API.
The image URLs are sent as a JSON response to the GET
request. These URLs are then used within the src
attribute of the img
tag to display the images on the app.
Update the src/routes/index.svelte
file like this.
1<script context="module">2 export function preload() {3 return this.fetch(`/index.json`)4 .then((r) => r.json())5 .then((images) => {6 return { images };7 });8 }9 </script>1011 <script>12 export let images;13 </script>1415 <svelte:head>16 <title>Sapper Cloudinary Example</title>17 </svelte:head>18 <h2>Sapper Image Gallery</h2>1920 <div class="container">21 {#each images as image (image.secureUrl)}22 <section class="image">23 <img src={image.secureUrl} alt={image.secureUrl} width="400px" />24 </section>25 {/each}26 </div>2728 <style>29 h2 {30 font-size: 2.4em;31 text-transform: uppercase;32 font-weight: 400;33 margin: 0 0 0.5em 0;34 text-align: center;35 }3637.container {38 column-count: 3;39 column-gap: 20px;40 }41 section > img {42 flex: 100%;43 max-width: 100%;44 margin-top: 1rem;45 border-radius: 10px;46 }47 .image {48 margin-bottom: 1rem;49 display: flex;50 }5152 @media only screen and (max-width: 600px) {53 .container {54 column-count: 1;55 }56 }57 </style>
You use Sapper's preload() function to fetch the images using this.fetch()
method. This preload()
function runs before the component is created and loads the data, i.e., images required by the gallery before the page is loaded.
1<script context="module">2 export function preload() {3 return this.fetch(`/index.json`)4 .then((r) => r.json())5 .then((images) => {6 return { images };7 });8 }9</script>
To display the images, you iterate over the images
array, exported from the script
tag, using the each
block. You can read more about each
block here.
1{#each images as image (image.secureUrl)}2<section class="image">3 <img src="{image.secureUrl}" alt="{image.secureUrl}" width="400px" />4</section>5{/each}
Here is how this Sapper Image Gallery looks like.
How To Upload Images to Cloudinary
In this section, we will write the code to upload the image to Cloudinary and then show it in the Image Gallery created in the last section.
Update the script
tag in index.svelte
file like this.
1<script>2 export let images;3 let preview, fileinput;45 const onFileSelected = (e) => {6 let image = e.files[0];7 let reader = new FileReader();8 reader.readAsDataURL(image);9 reader.onload = (e) => {10 preview = e.target.result;11 };12 };1314 const uploadImage = () => {15 uploadImageToCloudinary(preview);16 };1718 const uploadImageToCloudinary = async (imageDataUrl) => {19 const res = await fetch("/index.json", {20 method: "POST",21 headers: {22 "Content-Type": "application/json",23 },24 body: JSON.stringify({ imageDataUrl }),25 });26 const json = await res.json();27 if (json.imageUrl) {28 window.location.reload();29 }30 };31</script>
Before uploading the image to Cloudinary, the web application needs to read the selected picture; this is done with the help of FileReader Web API.
The files
object containing the selected file, obtained either from the FileList
object when the user selects a file using the <input>
element or drag and drop operations' DataTransfer
object is passed to the FileReader
Web API. In this project, you will use both the <input>
element and drag and drop operation.
The FileReader
Web API reads the user's selected file and then converts it to a data:
URL using the .readAsDataURL()
method. You can read more about this method here.
Using the .onload()
method of FileReader,
triggered each time the reading operation is successfully completed, the data:
URL is stored inside the preview
variable.
You have created another function named uploadImageToCloudinary ()
which takes the data URL as an argument and makes a POST
request to /index.json
using the fetch
API. The data URL is sent to the server route as a JSON object.
Once the image has been uploaded, and a successful response is returned from the POST request, the entire page is reloaded to trigger the Sapper's preload()
function to update the Image Gallery with the newly uploaded image.
Add the code for the Upload component before the code for Image Gallery in the index.svelte
file.
1<div class="app">2 <h2>Upload Image</h2>3 <div4 class="dropzone"5 on:click={() => {6 fileinput.click();7 }}8 on:drop={(e) => {9 e.preventDefault();10 onFileSelected(e.dataTransfer);11 }}12 on:dragenter={(e) => {13 e.preventDefault();14 }}15 on:dragleave={(e) => {16 e.preventDefault();17 }}18 on:dragover={(e) => {19 e.preventDefault();20 }}21 >22 {#if preview}23 <img class="preview" src={preview} alt="preview" />24 {/if}2526 <img27 class="upload-icon"28 src="https://static.thenounproject.com/png/625182-200.png"29 alt=""30 on:click={() => {31 fileinput.click();32 }}33 />34 <input35 style="display:none"36 type="file"37 accept=".jpg, .jpeg, .png"38 on:change={(e) => onFileSelected(e.target)}39 bind:this={fileinput}40 />41 {#if !preview}42 <div43 class="chan"44 on:click={() => {45 fileinput.click();46 }}47 >48 Drag N Drop Images or Click on Upload.49 </div>50 {/if}51 </div>52 {#if preview}53 <button54 on:click={() => {55 uploadImage();56 }}>Upload Image</button57 >58 {/if}59</div>
You can refer to the code to style the Upload component here.
In the above code, you create a div
element with class="app"
. In this div
element, you add the event handlers required for the drag and drop operation. The on:dragenter
, on:dragover
, and on:dragover
event just the page from reloading. The on:drop()
event, triggered when the file is dropped on the div
, passes the DataTransfer
object to the onSelectedFile()
function.
You bind the fileinput
variable to the input
element to get a reference to the input
element. This input
element only accepts .jpg, .jpeg, .png
image types. You can change the accepted image types according to your application needs.
When the upload icon is clicked, the on:click()
event is triggered, which runs the fileinput.click()
function and the user is prompted to select the image.
After the user has selected the image, the on:change()
event of the input
element is triggered, which passes the FileList
object to the onSelectedFile()
function.
Whether an image is selected or not, either the Upload button is displayed or the text Drag N Drop Images or Click on Upload.
is shown using the if
block and preview
variable.
Here is how the Upload Component looks when no file is selected.
Here is how the Upload Component changes if an image is selected.
When the upload button is clicked, the uploadImage()
function sends the POST request to the /index.json
server route with the data URL of the image in the request body, and the image is uploaded to Cloudinary.
In the above code, you are sending the image data URL as a JSON request body. To parse this JSON request body in the POST
request, you will need to install the body-parser
package. You can read more about this library here.
Run the following command in your terminal to install body-parser
.
1npm install body-parser
You will also need to update the src/server.js
file to include the body-parser
library.
1import sirv from "sirv";2import polka from "polka";3import compression from "compression";4import * as sapper from "@sapper/server";5const bodyParser = require("body-parser");67const { PORT, NODE_ENV } = process.env;8const dev = NODE_ENV === "development";910polka() // You can also use Express11 .use(12 bodyParser.json({13 limit: "50mb",14 extended: true,15 }),16 bodyParser.urlencoded({17 limit: "50mb",18 extended: true,19 }),2021 compression({ threshold: 0 }),22 sirv("static", { dev }),23 sapper.middleware()24 )25 .listen(PORT, (err) => {26 if (err) console.log("error", err);27 });
The last step is to create the POST
method in the index.json.js
file or the /index.json
server route.
Add the following code for the POST
method in the index.json.js
file.
1export function post(req, res) {2 const image = req.body.imageDataUrl;34 res.writeHead(200, {5 "Content-Type": "application/json",6 });78 cloudinary.uploader.upload(image).then((response) => {9 res.end(JSON.stringify({ imageUrl: response.secure_url }));10 });11}
The above code uses Cloudinary's Upload API to upload the image to Cloudinary. After a successful response, the Cloudinary URL of the image is returned as a JSON response.
You can also pass additional and optional parameters to the Upload API like the upload_preset
, signature
, folder
, etc.
Conclusion
In this Media Jam, we discussed how to build an Image Gallery in Sapper with images being fetched from Cloudinary. We also saw how to upload images to Cloudinary using both the <input>
element and drag and drop operation.
Here are some additional resources that can be helpful:
Happy coding!