DEV Community

Cover image for Building a Photo Uploader: A Practical Introduction to the JAMSTACK with Vue.js
JekayinOluwa Olabemiwo for Hackmamba

Posted on

Building a Photo Uploader: A Practical Introduction to the JAMSTACK with Vue.js

Introduction

JAMSTACK is a JavaScript-powered stack that enables you to harness the powers of JavaScript, APIs, and markups. You can build with the JAMSTACK by using the various JavaScript frameworks and libraries and integrate with serverless functions or APIs. It is fast, lean and inexpensive.

In this tutorial, you will learn how to build JAMSTACK apps with a feature to upload images. You will use Cloudinary to power the upload functionality and Auth0 for authentication. This can come in handy when building photo albums, e-commerce applications, and business websites.

For a brief introduction to Vue.js, you can check out this article, Getting Started with Vue JS: The Progressive JavaScript Framework.

Prerequisites

  1. A machine with Node.js and npm) installed.

  2. Basic familiarity with JavaScript, and ES6 and Vue.js).

  3. A text editor. E.g. Sublime Text, Visual Studio Code, Atom, etc.

  4. A Cloudinary account. You can sign up for a Cloudinary account for free.

  5. An Auth0 account. You may create a free Auth0 account here.

Install the Project Dependencies

You need the Node.js runtime and the npm to use Vue.js. The Node package manager, npm will enable you to use Node.js on the command-line interface, CLI. If you do not have Node.js or npm installed, you can follow the instructions here to install them on your machine.

Another tool that you need to follow in this tutorial is the Vue CLI package. The vue library will provide the capabilities of the Vue.js framework while vue-cli will enable you to use certain terminal commands to interact with Vue.js and your project.

You can go ahead to install Vue and the vue-cli tool if you have Node.js and npm on your machine already. Use the following terminal commands to install the two packages respectively.

npm install vue
npm install -g @vue/cli
Enter fullscreen mode Exit fullscreen mode

In the next section, you will set up your Vue.js project.

Create Vue Project

Use the following vue-cli command to set up a Vue.js project on your terminal. You may name the project vue-photo-uploader.

vue create vue-photo-uploader
Enter fullscreen mode Exit fullscreen mode

Select Vue.js 2 version in the options prompted and allow Vue to scaffold the project folders and files for you. You should get a message that looks like the following in your terminal.


📄 Generating README.md...

🎉 Successfully created project vue-photo-uploader.
👉 Get started with the following commands:

Enter fullscreen mode Exit fullscreen mode

Now, you need to set up the new project to handle file uploads. This is the purpose of the next section of the tutorial.

Configue the Vue.js Project for File Uploads

To handle file uploads, you will need to create an interface for your users to be able to upload files. To do that, modify the App.vue file of your project as follows:

<template>
  <div id="app">
    <div>
      <img alt="Vue logo" src="./assets/logo.png">
      <HelloWorld msg="Learn how to upload photos with Cloudinary"/>
    </div>

    <div>
        <div>
            <button @click="openUploadModel">Add Photo</button>
        </div>
    </div>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

In the above code, you edited the message to be displayed by the HelloWorld component. You changed it to learn how to upload photos with Cloudinary. In addition, you created a button that calls the openUploadModel method. The method will open the Cloudinary upload modal when the button is clicked. Now, add the method inside the part of the App.vue file as follows:

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'App',
  components: {
    HelloWorld,
  },
  methods: {
    openUploadModel() {
     }
}
</script>

The full code of the App.vue file becomes:

<template>
  <div id="app">
    <div>
      <img alt="Vue logo" src="./assets/logo.png">
      <HelloWorld msg="Learn how to upload photos with Cloudinary"/>
    </div>

    <div>
        <div>
            <button @click="openUploadModel">Add Photo</button>
        </div>
    </div>
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'App',
  components: {
    HelloWorld,
  },
  methods: {
    openUploadModal() {
     }
}
</script>

Configure Cloudinary Upload Widget

Login and obtain parameters

Login to your Cloudinary account and go to your console dashboard and get your cloud name. Also, get your upload preset from your Cloudinary acount settings. Copy the two values somewhere because you will need them in your Vue.js project.

Configure Upload Preset settings

In your account settings page, scroll down the Upload Settings tab to the Upload Preset section as shown below.

Upload preset settings on Cloudinary dashboard

Activate the unsigned uploading feature by clicking the Enable unsigned uploading link. The link should change to Signed as shown below.

Unsigned uploading feature enabled

If you don't enable unsigned upload preset, you might get the following error.

Upload preset must be whitelisted for unsigned uploads

Next, navigate to the index.html page in the public folder of your project. Then, include the Cloudinary widget script inside the

tag.
<body>
<script src="https://widget.cloudinary.com/v2.0/global/all.js" type="text/javascript"></script>
...
</body>

Next, modify the openUploadModal of the App.vue file as follows:

  ...
    openUploadModal() {
      window.cloudinary.openUploadWidget(
        { cloud_name: '<CLOUD_NAME>',
          upload_preset: '<UPLOAD_PRESET>'
        },
        (error, result) => {
          if (!error && result && result.event === "success") {
            console.log('Image uploaded..: ', result.info);  
          }
        }).open();
    }

In the above code, you added the logic of the @click event of the button by which calls the openUploadWidget method that activates the Cloudinary file upload modal.

Replace CLOUD_NAME and UPLOAD_PRESET in the above code with the values you got from your Cloudinary account.

Next, run the project on your terminal:

npm run serve

Then, navigate to 127.0.0.1:8000 on your browser to add images to the application. You should get a page that looks like the following image.

Photo album prototype live!

Click the Add Photo button and the upload modal will be displayed like the following.

Cloudinary upload widget

Now, you can upload your images.

Deliver and View Images with Cloudinary

Cloudinary enables the delivery of images to the browser via its Vue.js SDK, cloudinary-vue. Install the library to get started:

npm install cloudinary-vue 

You need the following Cloudinary Vue.js components to achieve the goal of this section of the article.

CldContext - this tag helps you to set the parameters that all the child components share.
CldImage - the Cloudinary Image tag. It sources images from Cloudinary and makes them available on the browser.
CldTransformation - the tag that allows you to describe the transformations that are applied to the images delivered.

Next, import the components in the <script> section of the App.vue file. In addition, add the image url and public_id to the Data property of Vue.js. These two variables will be defined later in the article.

<script>
...
import { CldContext, CldImage, CldTransformation } from 'cloudinary-vue'

export default {
  name: 'App',
  components: {
    HelloWorld,
    CldContext,
    CldImage,
    CldTransformation
  },
  data() {
    return {
      url: '',
      publicId: ''
    }
  },
...

</script>

Then, apply the imported components in the template code.

<template>
  <div id="app">
    ...

    <div>
    <cld-context cloudName="CLOUD_NAME">
      <div style="display: flex; justify-content: center;">
        <cld-image :publicId="publicId" width="250">
          <cld-transformation width="200" height="200" gravity="face" crop="thumb" />
         </cld-image>

         <cld-image :publicId="publicId" width="250">
          <cld-transformation width="200" height="200" gravity="face" crop="thumb" />
         </cld-image>

         <cld-image :publicId="publicId" width="250">
          <cld-transformation width="200" height="200" gravity="face" crop="thumb" />
         </cld-image>

         <cld-image :publicId="publicId" width="250">
          <cld-transformation width="200" height="200" gravity="face" crop="thumb" />
         </cld-image>

         <cld-image :publicId="publicId" width="250">
          <cld-transformation width="200" height="200" gravity="face" crop="thumb" />
         </cld-image>

      </div>
    </cld-context>
  </div>
  </div>
</template>

Don't forget to replace CLOUD_NAME in the code above with the real value gotten from your dashboard.

Whenever you upload an image, Cloudinary assigns a public_id to an image whenever you are uploading one.
Therefore, you can use the public_id returned in Cloudinary's JSON response when an image is being uploaded.

So, modify the openUploadModal() method to capture the public_id of an uploaded image.

    openUploadModal() {
      window.cloudinary.openUploadWidget(
        { cloud_name: 'CLOUD_NAME',
          upload_preset: 'UPLOAD_PRESET'
        },
        (error, result) => {
          if (!error && result && result.event === "success") {
            console.log('Image uploaded..: ', result.info);  

            //add the next 2 lines 
            this.url = result.info.url;
            this.publicId = result.info.public_id;
            }
        }).open();
    }

The full code of the App.vue file is the following:

<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Learn how to upload photos with Cloudinary"/>
    <div>
        <button @click="openUploadModel">Add Photo</button>
    </div>
  <div>
    <cld-context cloudName="diiayntjq">
      <div style="display: flex; justify-content: center;">
        <cld-image :publicId="publicId" width="250">
          <cld-transformation width="600" height="600" gravity="face" crop="thumb" />
         </cld-image>

      </div>
    </cld-context>
  </div>

  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'
import { CldContext, CldImage, CldTransformation } from 'cloudinary-vue'

export default {
  name: 'App',
  components: {
    HelloWorld,
    CldContext,
    CldImage,
    CldTransformation
  },
  data() {
    return {
      url: '',
      publicId: ''
    }
  },
  methods: {
    openUploadModel() {
      window.cloudinary.openUploadWidget(
        { cloud_name: 'diiayntjq',
          upload_preset: 'bi7uln2q'
        },
        (error, result) => {
          if (!error && result && result.event === "success") {
            console.log('Image uploaded..: ', result.info);  

            this.url = result.info.url;
            this.publicId = result.info.public_id;
          }
        }).open();
    }
  }    
}
</script>

Authenticating with Auth0

Auth0 allows you to authenticate and authorize users. It integrates with several languages and frameworks for building applications. Furthermore, Auth0 proves a Universal Login page to help with authenticating users. The login page redirects users back to your application after they log in.

Create an Auth0 Application

To start the authentication setup, log in to your Auth0 dashboard and follow the following steps to create an Auth0 Application.

  1. Click "Applications" in the left sidebar menu. Supply these parameters:
    • Name: Vue.js Image Uploader
    • Application type as Single Page Applications
  2. Click "Create" button to complete the application creation
  3. Click on the "Settings" tab of the newly created Application page. Set the following parameters:
    • Allowed Callback URLs: http://localhost:8080
    • Allowed Logout URLs: http://localhost:8080
    • Allowed Web Origins: http://localhost:8080
  4. Scroll down the page and click on the "Save Changes" button to save the settings.

Install Auth0
Install the Client SDK for Auth0 Single-Page Applications:

npm install @auth0/auth0-spa-js

Since you need to move from one page or link to another in the Vue.js application, you will need the Vue router. Add it with the following command and choose 'yes' when asked if you'd like to use it in history mode.

vue add router

Create a new folder inside the src folder and name it auth. Then, create a new file inside the auth folder and name it index.js. Add the following code inside the file.

import Vue from "vue";
import createAuth0Client from "@auth0/auth0-spa-js";

/** Define a default action to perform after authentication */
const DEFAULT_REDIRECT_CALLBACK = () =>
  window.history.replaceState({}, document.title, window.location.pathname);

let instance;

/** Returns the current instance of the SDK */
export const getInstance = () => instance;

/** Creates an instance of the Auth0 SDK. If one has already been created, it returns that instance */
export const useAuth0 = ({
  onRedirectCallback = DEFAULT_REDIRECT_CALLBACK,
  redirectUri = window.location.origin,
  ...options
}) => {
  if (instance) return instance;

  // The 'instance' is simply a Vue object
  instance = new Vue({
    data() {
      return {
        loading: true,
        isAuthenticated: false,
        user: {},
        auth0Client: null,
        popupOpen: false,
        error: null
      };
    },
    methods: {
      /** Authenticates the user using a popup window */
      async loginWithPopup(options, config) {
        this.popupOpen = true;

        try {
          await this.auth0Client.loginWithPopup(options, config);
          this.user = await this.auth0Client.getUser();
          this.isAuthenticated = await this.auth0Client.isAuthenticated();
          this.error = null;
        } catch (e) {
          this.error = e;
          // eslint-disable-next-line
          console.error(e);
        } finally {
          this.popupOpen = false;
        }

        this.user = await this.auth0Client.getUser();
        this.isAuthenticated = true;
      },
      /** Handles the callback when logging in using a redirect */
      async handleRedirectCallback() {
        this.loading = true;
        try {
          await this.auth0Client.handleRedirectCallback();
          this.user = await this.auth0Client.getUser();
          this.isAuthenticated = true;
          this.error = null;
        } catch (e) {
          this.error = e;
        } finally {
          this.loading = false;
        }
      },
      /** Authenticates the user using the redirect method */
      loginWithRedirect(o) {
        return this.auth0Client.loginWithRedirect(o);
      },
      /** Returns all the claims present in the ID token */
      getIdTokenClaims(o) {
        return this.auth0Client.getIdTokenClaims(o);
      },
      /** Returns the access token. If the token is invalid or missing, a new one is retrieved */
      getTokenSilently(o) {
        return this.auth0Client.getTokenSilently(o);
      },
      /** Gets the access token using a popup window */

      getTokenWithPopup(o) {
        return this.auth0Client.getTokenWithPopup(o);
      },
      /** Logs the user out and removes their session on the authorization server */
      logout(o) {
        return this.auth0Client.logout(o);
      }
    },
    /** Use this lifecycle method to instantiate the SDK client */
    async created() {
      // Create a new instance of the SDK client using members of the given options object
      this.auth0Client = await createAuth0Client({
        ...options,
        client_id: options.clientId,
        redirect_uri: redirectUri
      });

      try {
        // If the user is returning to the app after authentication..
        if (
          window.location.search.includes("code=") &&
          window.location.search.includes("state=")
        ) {
          // handle the redirect and retrieve tokens
          const { appState } = await this.auth0Client.handleRedirectCallback();

          this.error = null;

          // Notify subscribers that the redirect callback has happened, passing the appState
          // (useful for retrieving any pre-authentication state)
          onRedirectCallback(appState);
        }
      } catch (e) {
        this.error = e;
      } finally {
        // Initialize our internal authentication state
        this.isAuthenticated = await this.auth0Client.isAuthenticated();
        this.user = await this.auth0Client.getUser();
        this.loading = false;
      }
    }
  });

  return instance;
};

// Create a simple Vue plugin to expose the wrapper object throughout the application
export const Auth0Plugin = {
  install(Vue, options) {
    Vue.prototype.$auth = useAuth0(options);
  }
};

The options object in the above code is used to pass in the domain and clientId values. Hence, you have to create a JSON file called auth_config.json in the root folder of the Vue.js application and add the following values to it. Obtain the domain and clientId values from the "Settings" tab of the Auth0 Application page and put them in the JSON file like the following.

{
  "domain": "YOUR_DOMAIN",
  "clientId": "YOUR_CLIENT_ID"
}

Next, import the Auth0 plugin and Vue router in the main.js file as shown below.

...
import router from './router' // newly added line

// Import the Auth0 configuration
import { domain, clientId } from "../auth_config.json";

// Import the plugin here
import { Auth0Plugin } from "./auth";

// Install the authentication plugin here
Vue.use(Auth0Plugin, {
  domain,
  clientId,
  onRedirectCallback: appState => {
    router.push(
      appState && appState.targetUrl
        ? appState.targetUrl
        : window.location.pathname
    );
  }
});

Vue.config.productionTip = false;

new Vue({
  router, // newly added line
  render: h => h(App)
}).$mount("#app");

Configure Log in and Log out follows

Edit the App.vue file by adding two buttons for login and log out after the HelloWorld tag in the template code.

<template>
  <div id="app">
  ...

    <div v-if="!$auth.loading">
        <!-- show login when not authenticated -->
        <button v-if="!$auth.isAuthenticated" @click="login">Log in</button>
        <!-- show logout when authenticated -->
        <button v-if="$auth.isAuthenticated" @click="logout">Log out</button>
    </div>

  ...
  </div>
</template>

Next, add the login and log out methods in the list of methods inside the <script> section

...
  methods: {
    // Log the user in
    login() {
      this.$auth.loginWithRedirect();
    },
    // Log the user out
    logout() {
      this.$auth.logout({
        returnTo: window.location.origin
      });
    },
    openUploadModel() {
      ...

Now, you have added the login and log-out buttons and can proceed to test the authentication flow. Go to 127.0.0.1:8000 on your web browser. You will see the login button present now as shown below.

Login button is visible on the page now

When you click the login button, you will be directed to the Auth0 Universal login Page where your users can sign up or log in after which they will be redirected to your application.

Auth0 universal login page activated

You can study the The Complete Guide to Vue.js User Authentication with Auth0 for a detailed explanation of the Auth0 implementation in Vue.js.

Conclusion

With the knowledge gained, you can try implementing the image and video upload in your next project. You may also develop a full-fledged user profile with Auth0. Much more, you can learn about the JAMSTACK and its ecosystem as you build more JAMSTACK projects.

Thanks for your attention.

Content created for the Hackmamba Jamstack Content Hackathon using Auth0, Cloudinary and Vue.js.

Discussion (1)

Collapse
hazelday profile image
HazelDay

that is really amazing and valuable information you have shared . thanks for sharing . Apartments for rent london england