DEV Community

Cover image for Building a PhotoShare App Using Auth0, Cloudinary, React.JS and Supabase.
ADESANYA JOSHUA AYODEJI for Hackmamba

Posted on

8 2

Building a PhotoShare App Using Auth0, Cloudinary, React.JS and Supabase.

Photoshare is an application where users can login using Auth0 and share picture that can be viewed by other users using Cloudinary.

Auth0 is an easy to use authentication and authorization platform, it takes away the stress of authentication and authorization during the building process.

Cloudinary is a service that makes life easy when it comes to working with images, you can upload images, resize images, crop images and other cool stuffs without installing any complex software or going through heavy documentation.

Supabase is a firebase alternative, it is very useful in setting up a backend service in few minutes.

Perequisites Knowledge

  • React Js
  • CSS

Lets Start Building

Setup React

I am assuming we can setup react on our own. In the case when you are unable to set up react.js on your own, check out this tutorial by freecodecamp - How to setup react js

We need to flesh up our application to make it usable for the demo, i will drop some snippet, all you have to do is replace then in the appropriate files, i will explain where i need to.

index.html

<!DOCTYPE html>
<html lang="en" class="h-100">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
rel="shortcut icon"
href="https://res.cloudinary.com/josh4324/image/upload/v1633549736/new_xhdbfr.png"
/>
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Share pictures with your friends."
/>
<link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk"
crossorigin="anonymous"
/>
<link
rel="stylesheet"
href="https://cdn.auth0.com/js/auth0-samples-theme/1.0/css/auth0-theme.min.css"
/>
<script src="https://kit.fontawesome.com/3116610f1c.js"></script>
<script src="https://upload-widget.cloudinary.com/global/all.js" type="text/javascript"></script>
<title>Photo Share App</title>
</head>
<body class="h-100">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root" class="h-100"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
view raw index.html hosted with ❤ by GitHub

The index.html file is inside the public folder.

Create a component folder inside your src folder, we will be creating few components.

main-nav.js

import {NavLink} from "react-router-dom";
import React from "react";
const MainNav = () => (
<div className="navbar-nav mr-auto">
<NavLink
to="/"
exact
className="nav-link"
activeClassName="router-link-exact-active"
>
Home
</NavLink>
<NavLink
to="/profile"
exact
className="nav-link"
activeClassName="router-link-exact-active"
>
Profile
</NavLink>
</div>
);
export default MainNav;
view raw main-nav.js hosted with ❤ by GitHub

nav-bar.js

import React from "react";
import MainNav from "./main-nav";
const NavBar = () => {
return (
<div className="nav-container mb-3">
<nav className="navbar navbar-expand-md navbar-light bg-light">
<div className="container">
<div className="navbar-brand logo1" />
<MainNav />
</div>
</nav>
</div>
);
};
export default NavBar;
view raw nav-bar.js hosted with ❤ by GitHub

footer.js

view raw footer.js hosted with ❤ by GitHub

loading.js

import React from "react";
const Loading = () => (
<div className="spinner">
<div className= "loader" style={{marginTop:"20%"}}> </div>
</div>
);
export default Loading;
view raw loading.js hosted with ❤ by GitHub

index.js
import Footer from "./footer";
import Loading from "./loading";
import NavBar from "./nav-bar";
export { Footer,Loading, NavBar };
view raw index.js hosted with ❤ by GitHub

We are done with our components, now we need to create pages that will make use of the components.

Create a views folder inside the src folder.

The following pages will be inside the views folder

home.js

import React, { Fragment } from "react";
export default function Home() {
return (
<Fragment>
</Fragment>
)
}
view raw home.js hosted with ❤ by GitHub

profile.js

import React from "react";
const Profile = () => {
return (
<div>Profile</div>
);
};
export default Profile;
view raw profile.js hosted with ❤ by GitHub

index.js

import Home from "./home";
import Profile from "./profile";
export { Home, Profile };
view raw index.js hosted with ❤ by GitHub

We are done with the views folder for now. The only files remaining for us to fill up are index.js, app.js and app.css.

index.js

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { BrowserRouter as Router } from "react-router-dom";
ReactDOM.render(
<Router>
<App />
</Router>,
document.getElementById("root")
);
view raw index.js hosted with ❤ by GitHub

app.js

import React from "react";
import { Route, Switch } from "react-router-dom";
import { NavBar, Footer, Loading } from "./components";
import { Home, Profile } from "./views";
import "./app.css";
const App = () => {
return (
<div id="app" className="d-flex flex-column h-100">
<NavBar />
<div className="container flex-grow-1">
<Switch>
<Route path="/" exact component={Home} />
<Route path="/profile" component={Profile} />
</Switch>
</div>
<Footer />
</div>
);
};
export default App;
view raw app.js hosted with ❤ by GitHub

app.css

.next-steps .fa-link {
margin-right: 5px;
}
/* Fix for use only flexbox in content area */
.next-steps .row {
margin-bottom: 0;
}
.next-steps .col-md-5 {
margin-bottom: 3rem;
}
@media (max-width: 768px) {
.next-steps .col-md-5 {
margin-bottom: 0;
}
}
.spinner {
position: absolute;
display: flex;
justify-content: center;
height: 100vh;
width: 100vw;
background-color: white;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.result-block-container .result-block {
opacity: 1;
}
.shareInput{
width: 100%;
height: 100%;
padding: .5em;
resize: none;
border: none;
}
.share {
border-radius: 10px;
-webkit-box-shadow: 0px 0px 16px -8px rgba(0, 0, 0, 0.68);
box-shadow: 0px 0px 16px -8px rgba(170, 157, 157, 0.68);
width: 60%;
margin-left: auto;
margin-right: auto;
padding: 10px;
}
.post-list {
width: 60%;
margin-left: auto;
margin-right: auto;
margin-top: 30px;
}
.shareInput:focus {
outline: none;
}
.attachments--btn {
color: #007582;
}
input[type="file"] {
display: none;
}
.shareButton{
border: none;
border-radius: 5px;
background-color: blue;
font-weight: 300;
cursor: pointer;
color: white;
padding-left: 10px;
padding-right: 10px;
height: 30px;
margin-top: 10px;
}
.shareImgContainer{
padding: 0 20px 10px 20px;
position: relative;
}
.shareImg{
width: 50%;
height: 50%;
object-fit: cover;
}
.shareCancelImg{
position: absolute;
top: 0;
right: 20px;
cursor: pointer;
opacity: 0.7;
}
.logo1 {
background-image: url("https://res.cloudinary.com/josh4324/image/upload/v1633549736/new_xhdbfr.png") !important;
width: 50px;
height: 50px;
background-repeat: no-repeat;
background-size: cover;
}
.loader,
.loader:before,
.loader:after {
border-radius: 50%;
width: 2.0em;
height: 2.0em;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
-webkit-animation: load7 1.8s infinite ease-in-out;
animation: load7 1.8s infinite ease-in-out;
}
.loader {
color: rgb(67, 67, 185);
font-size: 10px;
margin-bottom: 50px;
margin-left: auto;
margin-right: auto;
position: relative;
text-indent: -9999em;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
}
.loader:before,
.loader:after {
content: '';
position: absolute;
top: 0;
}
.loader:before {
left: -3.5em;
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
}
.loader:after {
left: 3.5em;
}
@-webkit-keyframes load7 {
0%,
80%,
100% {
box-shadow: 0 2.5em 0 -1.3em;
}
40% {
box-shadow: 0 2.5em 0 0;
}
}
@keyframes load7 {
0%,
80%,
100% {
box-shadow: 0 2.5em 0 -1.3em;
}
40% {
box-shadow: 0 2.5em 0 0;
}
}
view raw app.css hosted with ❤ by GitHub

Install Dependencies in react.
@auth0/auth0-react,
@material-ui/core,
@material-ui/icons,
@supabase/supabase-js,
react-router-dom,
date-fns

Add Auth0 to your React Application.

Sign up for a new account if you dont have one, once your signup is done Auth0 takes you to the dashboard, in the left side menu, click on applications.

On the applications page, click on the create application button.

You need to enter the name of the app and choose the application type.
You can use any name you want, we will select the single page web application, this is because we are using react.js.

When you are done, click on the Create button.

The next step is to add some urls on the application settings page.

Make sure you are on the settings page for the application, you just created.
The following fields needs to filled

  • Allowed Callback URLs
  • Allowed Logout URLs
  • Allowed Web Origins

The base url of the application shoud be entered into the fields above i.e localhost:300 or appzone.com. Make sure you save the changes at the bottom of the page.

Add the Auth0 configuration variables to React

Create a .env inside the the src folder, populate the following fields

REACT_APP_AUTH0_DOMAIN=
REACT_APP_AUTH0_CLIENT_ID=
Enter fullscreen mode Exit fullscreen mode

The values can be found on your Auth0 application settings page.

The first one is the domain value from the settings.
The second one is the client value from the settings.

The react application can now be able to interact with the Auth0 authorization server.

Set up the Auth0 React SDK

The Auth0 react dependency has been installed - @auth0/auth0-react

We need to create a auth folder, where we would have all our authentication files.

We need to create a Auth0Provider file inside the auth folder to setup Auth0 for react.

src/auth/auth0-provider.js

import React from "react";
import { useHistory } from "react-router-dom";
import { Auth0Provider } from "@auth0/auth0-react";
const AuthProvider = ({ children }) => {
const history = useHistory();
const domain = process.env.REACT_APP_AUTH0_DOMAIN;
const clientId = process.env.REACT_APP_AUTH0_CLIENT_ID;
const onRedirectCallback = (appState) => {
history.push(appState?.returnTo || window.location.pathname);
};
return (
<Auth0Provider
domain={domain}
clientId={clientId}
redirectUri={window.location.origin}
onRedirectCallback={onRedirectCallback}
>
{children}
</Auth0Provider>
);
};
export default AuthProvider;

We need to integrate the Auth0 provider in the index.js, for that to happen, we need to edit out index.js inside the src folder.

src/index.js

import React from "react";
import ReactDOM from "react-dom";
import App from "./app";
import { BrowserRouter as Router } from "react-router-dom";
import AuthProvider from "./auth/auth0-provider";
ReactDOM.render(
<Router>
<AuthProvider>
<App />
</AuthProvider>
</Router>,
document.getElementById("root")
);
view raw index.js hosted with ❤ by GitHub

At this point, we can run npm start to start up the application to be sure everything is running fine.

Up next, we will start adding our Login, Signup and Logout button from Auth0.

We will create our login-button.js, signup-button.js and logout-button.js in the components folder.

login-button.js

import React from "react";
import { useAuth0 } from "@auth0/auth0-react";
const LoginButton = () => {
const { loginWithRedirect } = useAuth0();
return (
<button
className="btn btn-primary btn-block"
onClick={() => loginWithRedirect()}
>
Log In
</button>
);
};
export default LoginButton;
view raw login-button.js hosted with ❤ by GitHub

We made use of the useAuth0 hook, we got the loginWithRedirect from it, which is useful for our login button.

signup-button.js

import React from "react";
import { useAuth0 } from "@auth0/auth0-react";
const SignupButton = () => {
const { loginWithRedirect } = useAuth0();
return (
<button
className="btn btn-primary btn-block"
onClick={() =>
loginWithRedirect({
screen_hint: "signup",
})
}
>
Sign Up
</button>
);
};
export default SignupButton;

logout-button.js

import React from "react";
import { useAuth0 } from "@auth0/auth0-react";
const LogoutButton = () => {
const { logout } = useAuth0();
return (
<button
className="btn btn-danger btn-block"
onClick={() =>
logout({
returnTo: window.location.origin,
})
}
>
Log Out
</button>
);
};
export default LogoutButton;

We made use of the useAuth0 hook, we got the logout from it, which is useful for our logout button.

Up next, lets integrate our login and logout button, so that when we are logged in we see the logout button and when we are logged out, we see the login button.

authentication-button.js

import React from "react";
import LoginButton from "./login-button";
import LogoutButton from "./logout-button";
import { useAuth0 } from "@auth0/auth0-react";
const AuthenticationButton = () => {
const { isAuthenticated } = useAuth0();
return isAuthenticated ? <LogoutButton /> : <LoginButton />;
};
export default AuthenticationButton;

Here we got the isAuthenticated from our useAuth0 hook, this is needed to track when we are logged in or logged out.

Now, we need to create auth-nav.js which will contain our AuthenticationButton.

auth-nav.js

import React from "react";
import AuthenticationButton from "./authentication-button";
const AuthNav = () => (
<div className="navbar-nav ml-auto">
<AuthenticationButton />
</div>
);
export default AuthNav;
view raw auth-nav.js hosted with ❤ by GitHub

To bring everything together, lets update the nav-bar.js

nav-bar.js

import React from "react";
import MainNav from "./main-nav";
import AuthNav from "./auth-nav";
const NavBar = () => {
return (
<div className="nav-container mb-3">
<nav className="navbar navbar-expand-md navbar-light bg-light">
<div className="container">
<div className="navbar-brand logo1" />
<MainNav />
<AuthNav />
</div>
</nav>
</div>
);
};
export default NavBar;
view raw nav-bar.js hosted with ❤ by GitHub

At this point, we can test our application, you should be able to signup, login and logout using Auth0.

Upnext, we need to secure our routes and access some information from Auth0

We will create protected-route.js in the auth folder.

protected-route.js

import React from "react";
import { Route } from "react-router-dom";
import { withAuthenticationRequired } from "@auth0/auth0-react";
import { Loading } from "../components/index";
const ProtectedRoute = ({ component, ...args }) => (
<Route
component={withAuthenticationRequired(component, {
onRedirecting: () => <Loading />,
})}
{...args}
/>
);
export default ProtectedRoute;

We can now protect all our routes in the app.js file.

app.js

import React from "react";
import { Route, Switch } from "react-router-dom";
import { NavBar, Footer, Loading } from "./components";
import { Home, Profile } from "./views";
import ProtectedRoute from "./auth/protected-route";
import "./app.css";
const App = () => {
return (
<div id="app" className="d-flex flex-column h-100">
<NavBar />
<div className="container flex-grow-1">
<Switch>
<ProtectedRoute exact path="/" component={Home} />
<ProtectedRoute path="/profile" component={Profile} />
</Switch>
</div>
<Footer />
</div>
);
};
export default App;
view raw app.js hosted with ❤ by GitHub

At this point, we can test our application, you should not be able to access the home page and login page. it will redirect you to the an Auth0 login modal when you are not logged in.

Setup Cloudinary

If you dont have a cloudinary account, signup on cloudinary.com

First step, we need to add this script to the index.html in the public folder

We need to create two function in the home.js file, we would make use of it inside the file.

const myWidget = window.cloudinary.createUploadWidget({
cloudName: cloudname,
uploadPreset: presetname}, (error, result) => {
if (!error && result && result.event === "success") {
console.log(result);
console.log('Done! Here is the image info: ', result.info);
setFile(result.info.secure_url)
}
}
)
const showWidget = () => {
setFile(null);
myWidget.open();
}
view raw cloud.js hosted with ❤ by GitHub

The cloudname can be gotten on cloudinary dashboard while the presetname can be gotten settings page, upload tab.


SetUp Supabase

To create a supabase account, go to supabase

After signup is complete, click on new project

Chose the existing organisation.

Fill the create new project form.

Click on the create new project button to complete the form.

The setup process runs for a few minutes.

Once it is done, It will show you the project dashboard, you will see a card which is titled Database, click on the table editor in the card.

Click on create a new table.

Enter the table name and description.

You will also need to add columns to the table, two default columns are already added.

For the columns, you need to enter the name, type(i.e int) and the default value, you can also specify if you want the column to be the Primary Key.

What i choose for the Demo

Table name - Image
Columns (type)

  • userId (varchar)
  • image (text)
  • likes (int)
  • dislikes(int)
  • desc(text)

Supabase is good to go and ready to be used.

Intergrate Supabase with React

We will create a client.js file in our src folder.

client.js

import {createClient} from '@supabase/supabase-js';
export const supabase = createClient(
config-url,
token
)
view raw client.js hosted with ❤ by GitHub

To get these detail go the settings page of your supabase dashboard.

For the config_url, you will get it on the config card, the name of the card is config and the name of the detail you need is URL.

For the token, the name of the card is Project API keys and the name of the anon public.

Finish Up App

Home.js

import React, { Fragment, useRef, useState, useEffect } from "react";
import { supabase } from "../client";
import formatDistanceToNow from "date-fns/formatDistanceToNow";
import { PermMedia, Cancel } from "@material-ui/icons";
import { useAuth0 } from "@auth0/auth0-react";
export default function Home() {
const desc = useRef("");
const [file, setFile] = useState(null);
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const { user } = useAuth0();
const { picture, nickname, sub } = user;
const getAllImages = async () => {
const { data } = await supabase.from("Image").select();
let newData = data.reverse();
setData(newData);
};
const createPost = async () => {
setLoading(true);
let newPost = {
image: file,
desc: desc.current.value,
likes: 0,
dislikes: 0,
userId: sub,
userimage: picture,
name: nickname
};
await supabase.from("Image").insert(newPost).single();
setFile(null);
setLoading(false);
getAllImages();
};
const myWidget = window.cloudinary.createUploadWidget(
{
cloudName: "josh4324",
uploadPreset: "hq1e5jub"
},
(error, result) => {
if (!error && result && result.event === "success") {
console.log(result);
console.log("Done! Here is the image info: ", result.info);
setFile(result.info.secure_url);
}
}
);
const showWidget = () => {
setFile(null);
myWidget.open();
};
useEffect(() => {
getAllImages();
return () => {};
}, [file]);
return (
<Fragment>
<div className="share">
<div className="shareWrapper">
<div className="shareTop">
<textarea
placeholder={"Share your picture with awesome captions"}
className="shareInput"
ref={desc}
></textarea>
</div>
</div>
<hr />
{file && (
<div className="shareImgContainer">
<img className="shareImg" src={file} alt="" />
<Cancel className="shareCancelImg" onClick={() => setFile(null)} />
</div>
)}
{<div className={loading === true ? "loader" : "none"}></div>}
<div
className="post-actions__attachments"
style={{ display: "flex", justifyContent: "space-between" }}
>
<button
type="button"
onClick={showWidget}
className="btn post-actions__upload attachments--btn"
>
<label
for="upload-image"
className="post-actions__label"
style={{ cursor: "pointer" }}
>
<PermMedia htmlColor="tomato" className="shareIcon" />
<span style={{ padding: "5px" }}>Photo</span>
</label>
</button>
<button className="shareButton" type="submit" onClick={createPost}>
Share
</button>
</div>
</div>
<div className="post-list">
<div style={{ width: "100%" }}>
{data.map((item) => {
return (
<div key={item.sub} className="col-md-12 grid-margin">
<div
className="card rounded"
style={{ marginBottom: "20px", width: "100%" }}
>
<div className="card-header">
<div className="d-flex align-items-start justify-content-between">
<div className="d-flex align-items-start">
<img
className=" rounded-circle"
style={{ width: "50px", height: "50px" }}
src={item.userimage}
alt=""
/>
<div className="ml-2">
<p style={{ margin: 0 }}>{item.name}</p>
<p className="">
{formatDistanceToNow(new Date(item.created_at), {
addSuffix: true
})}
</p>
</div>
</div>
</div>
</div>
<div className="card-body">
<img className="img-fluid mb-2" src={item.image} alt="" />
<p className="">{item.desc}</p>
</div>
</div>
</div>
);
})}
</div>
</div>
</Fragment>
);
}
view raw home.js hosted with ❤ by GitHub

A couple of things is going on in this file but it basically brings together everything we have been working on.

Three important things to note.

  1. we imported supabase from the client.js, this is what we would use to create post and get all the posts.
  2. We are making use of formatDistanceToNow from fns date package to get relative date.
  3. We are import useAuth0 form Auth0 package. This is where we get the user information such as name, unique id and picture.

We also added our two cloudinary powered functions, this is what is triggered when the image button is clicked and it pops up the cloudinary widget which we willl use to upload our image.

We also have two other functions powered by supabase, the first one is the create post, which is called when we submit our post, we also have the getAllImages function which triggers when the page reloads or when a post is created.

We also make use of useState to keep track of our states, useEffect to run functions when a page is reloaded and useRef to get data from elements.

We also obviously added some html and css to make it look a little bit nice.

I hope you have been able to learn a few things from the tutorial and the code snippets, in order to solidify your knowledge, you can complete the profile page, by displaying the user data and only the user’s posts on the page.

Thank You.

Link to the demo - https://jbwym.csb.app/

Content created for the Hackmamba Jamstack Content Hackathon with Auth0 and Cloudinary.

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (2)

Collapse
 
odionjulius4 profile image
Odion Julius

Really cool! Quite educative

Collapse
 
aboss123 profile image
Ashish Bailkeri

Nice job. Really good step by step instructions and explanation.

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay