Generate a Personalized Welcome Email Using Next.js, Amazon Polly, and the Editframe API

When a friend arrives at your house for a dinner party, you wouldn’t say “Hello, Guest! It’s great to see you. Please, allow me to fix you a beverage and provide you with sustenance.” You’d probably greet them by name, ask how they’re doing, and maybe offer them a drink or snack that you know they personally enjoy. You don’t talk to your friends in a generic and impersonal way, so why would you do this with your email subscribers?

Consumers today have come to expect personalization in their digital interactions. A recent McKinsey report shows that 71% of consumers expect companies to deliver personalized interactions, and 76% get frustrated when this doesn’t happen. Brands that deliver personalization effectively enjoy a wide range of benefits–like increased loyalty, lower churn risk, and higher customer lifetime value, to name just a few.

Since email can be delivered at key moments in the customer journey like when users begin a free trial or subscribe to a newsletter, it’s an ideal channel in which to deliver this personalized touch. Without the right tools, however, executing personalized email campaigns at scale can be a costly and time-consuming endeavor–especially for the developers who have to integrate email and marketing automation platforms with each other, and maintain these data flows as vendors update their API configurations.

Tutorial Introduction

In this tutorial, we will show you how to use Next.js, MongoDB, Amazon Polly, and the Editframe SDK to send a personalized welcome video email to each new guest who signs up to receive your content. That video will look like this:

Required tools

This project will make use of the following tools:

  • Next.js: Powers the user experience
  • MongoDB: Stores and serves user data
  • AWS Polly: Turns text into speech in the video
  • Editframe: Ties all of these elements together into a templatized video with a personalized voiceover

Using these simple and interoperable tools, you can build a memorable personalized experience into your first impression with customers while minimizing the engineering overhead required to do so.

Before we begin, you will need:

  • Node.js installed on your machine
  • Editframe API Token (you can create an account from this link)
  • Free AWS account (AWS Polly for text to speech service)

If you would like to view a finished version of this project as you read along with this tutorial, you can clone the repo here. Let’s get started!

Setup a Next.js project

  • Create a new Next.js project on your local machine using the create-next-app cli
npx create-next-app welcome-email-editframe
  • Navigate into this directory, and run a Next.js development server
cd welcome-email-editframe && yarn dev

Add an Authentication layer using NextAuth.js

  • Install NextAuth.js package to handle authentication and dotenv to load environment variables from your .env file
yarn add next-auth dotenv
  • Create a file called [...nextauth].js under a new path called pages/api/auth. (All requests to /api/auth/* (signIn, callback, signOut, etc.) will automatically be handled by NextAuth.js)
mkdir pages/api/auth && touch "pages/api/auth/[...nextauth].js"
  • Paste the code below inside the file you just created:
import "dotenv/config";
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
export const authOptions = {
  // Configure one or more authentication providers
  providers: [
    // Email & Password
    CredentialsProvider({
      id: "credentials",
      name: "Credentials",
      credentials: {
        email: {
          label: "Email",
          type: "text",
        },
        password: {
          label: "Password",
          type: "password",
        },
      },
      authorize(credentials) {
        return credentials;
      },
    }),
  ],
};
export default NextAuth(authOptions);

In the code above, we’re importing CredentialsProvider from NextAuth, which will handle email and password authentication for us. NextAuth also provides several other authentication methods that serve a variety of use cases. Note that the authorize method on our authOptions object accepts a credentials callback function. Right now, the method only returns the credentials themselves, but we will add validation logic when we work on the database connection later.

Now, let’s update pages/_app.js to include a NextAuth session provider which will perform a client-side authentication check to determine whether the user is logged in.

  • Update the code in In pages/_app.js as follows:
import '../styles/globals.css'

import { SessionProvider } from "next-auth/react"
export default function App({
  Component,
  pageProps: { session, ...pageProps },
}) {
  return (
    <SessionProvider session={session}>
      <Component {...pageProps} />
    </SessionProvider>
  )
}
  • Update pages/index.js (the homepage file) to add sign-in and logout buttons using useSession hook:

    import { useSession, signIn, signOut } from "next-auth/react"
    
    export default function Home() {
      const { data: session } = useSession()
      if (session) {
        return (
          <>
            Signed in as {session.user.email} <br />
            <button onClick={() => signOut()}>Sign out</button>
          </>
        )
      }
      return (
        <>
          Not signed in <br />
          <button onClick={() => signIn()}>Sign in</button>
        </>
      )
    }
    
  • Create a .env file in the root folder path:

    touch .env

  • Add the following variables to your .env file:

NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=SESSION_ENCRYPTION_STRING

Now the homepage of your app should display a “Sign in” button that, when clicked, prompts you to enter an email and password:

Add GIF of log in flow

Add the MongoDB connection utility and adapter

In this section, we will add a connection to a MongoDB database for storing user credentials, as well as an adapter for our NextAuth.js package which will allow us to validate user credentials at the database layer.

  • Install the MongoDB client, Mongoose (an Object Data Modeling library for MongoDB), the NextAuth MongoDB adapter, and bcrypt to hash user passwords:
  yarn add mongodb mongoose @next-auth/mongodb-adapter bcrypt
  • Create a lib directory for storing utility files:
  mkdir lib
  • Create a dbConnect.js file to initialize the Mongoose client:
touch lib/dbConnect.js
  • Add the following code to lib/dbConnect.js
/** 
Source : 
<https://github.com/vercel/next.js/blob/canary/examples/with-mongodb-mongoose/utils/dbConnect.js> 
**/
import mongoose from "mongoose";

if (!process.env.MONGO_USERNAME && process.env.MONGO_PASSWORD && process.env.MONGO_DB) {
  throw new Error("Please add your mongoDB variables to .env");
}

const MONGODB_URI = `mongodb+srv://${process.env.MONGO_USERNAME}:${process.env.MONGO_PASSWORD}@cluster0.ghiho.mongodb.net/${process.env.MONGO_DB}?retryWrites=true&w=majority`;

/**
 * Global is used here to maintain a cached connection across hot reloads
 * in development. This prevents connections growing exponentially
 * during API Route usage.
 */
let globalWithMongoose = global;
let cached = globalWithMongoose.mongoose;

if (!cached) {
  cached = globalWithMongoose.mongoose = { conn: null, promise: null };
}

async function dbConnect() {
  if (cached.conn) {
    return cached.conn;
  }

  if (!cached.promise) {
    const opts = {
      bufferCommands: false,
    };

    cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongoose) => {
      return mongoose;
    });
  }
  cached.conn = await cached.promise;
  return cached.conn;
}

export default dbConnect;
  • Create a mongodb.js file in the lib directory to initialize the MongoDB client:

    touch lib/mongodb.js
    
  • Add the following code to lib/mongodb.js

// This approach is taken from <https://github.com/vercel/next.js/tree/canary/examples/with-mongodb>
import "dotenv/config";
import { MongoClient } from "mongodb";

const uri = `mongodb+srv://${process.env.MONGO_USERNAME}:${process.env.MONGO_PASSWORD}@cluster0.ghiho.mongodb.net/${process.env.MONGO_DB}?retryWrites=true&w=majority`;

const options = {
  useUnifiedTopology: true,
  useNewUrlParser: true,
};

let client;
let clientPromise;

if (
  !process.env.MONGO_USERNAME &&
  !process.env.MONGO_PASSWORD &&
  !process.env.MONGO_DB
) {
  throw new Error("Please add your Mongo variables to .env");
}

if (process.env.NODE_ENV === "development") {
  // In development mode, use a global variable so that the value
  // is preserved across module reloads caused by HMR (Hot Module Replacement).
  if (!global._mongoClientPromise) {
    client = new MongoClient(uri, options);
      global._mongoClientPromise = client.connect();
      console.log("MongDB connected...")
  }
  clientPromise = global._mongoClientPromise;
} else {
  // In production mode, it's best to not use a global variable.
  client = new MongoClient(uri, options);
  clientPromise = client.connect();
}

// Export a module-scoped MongoClient promise. By doing this in a
// separate module, the client can be shared across functions.
export default clientPromise;
  • Create a directory for your data data models, and add a new file there for the User model:
mkdir models 
touch models/User.js
  • Paste the User schema code below into models/User.js
import mongoose from "mongoose";
const Schema = mongoose.Schema;

let User = new Schema({
  name: {
    type: String,
    required: true,
  },
  email: {
    type: String,
    required: true,
  },
  password: {
    type: String,
    required: true,
  },
  createdAt: {
    type: Date,
    required: true,
    default: new Date(),
  },
});

export default mongoose.models.User ||  mongoose.model("User", User);
  • Update […nextauth.js] to include the database connection:
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { MongoDBAdapter } from "@next-auth/mongodb-adapter";
import clientPromise from "../../../lib/mongodb";
import dbConnect from "../../../lib/dbConnect";
import User from "../../../models/User";
import { compare } from "bcrypt";

export default NextAuth({
  providers: [
    // Email & Password
    CredentialsProvider({
      id: "credentials",
      name: "Credentials",
      credentials: {
        email: {
          label: "Email",
          type: "text",
        },
        password: {
          label: "Password",
          type: "password",
        },
      },
      async authorize(credentials) {
        await dbConnect();

        console.log(credentials);
        // Find user with the email
        const user = await User.findOne({
          email: credentials?.email,
        });

        // Email Not found
        if (!user) {
          throw new Error("Email is not registered");
        }

        // Check hashed password with DB hashed password

        const isPasswordCorrect = await compare(
          credentials?.password,
          user.password
        );

        // Incorrect password
        if (!isPasswordCorrect) {
          throw new Error("Password is incorrect");
        }

        return user;
      },
    }),
  ],

  adapter: MongoDBAdapter(clientPromise),
});
  • Update .env file with new variables:
MONGO_DB=
MONGO_USERNAME=
MONGO_PASSWORD=

Establish a user account creation route

If you try to log in at this stage, you’ll encounter a server error saying Please add your Mongo variables to .env. Let’s create a route in our app that will allow a user to complete the account creation flow:

  • Create a register API route inside of the api directory:
touch pages/api/register.js
  • In pages/api/register.js, add the code below which will import the MongoDB connection function, User schema, and bcrypt for password hashing:
// Next.js API route support: <https://nextjs.org/docs/api-routes/introduction>
import dbConnect from "../../lib/dbConnect";
import User from "../../models/User";
import bcrypt from "bcrypt";

In pages/api/register.js:

  • Add a RegEx function to add a server-side email validation layer:
const validateEmail = (email) => {
  const regEx = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}$/i;
  return regEx.test(email);
};
  • Add a function to to check if email already exists in our database and confirm that the password length is greater than eight characters:
const validateForm = async (username, email, password) => {
  if (!validateEmail(email)) {
    return { error: "Email is invalid" };
  }

  await dbConnect();
  const emailUser = await User.findOne({ email: email });

  if (emailUser) {
    return { error: "Email already exists" };
  }

  if (password.length < 8) {
    return { error: "Password must have 8 or more characters" };
  }

  return null;
};
  • Add a function to validate the form inputs and handle password hashing. If the validation succeeds, the function will create a new user in our DB and return an API response:
export default async function handler(req, res) {
  // validate if it is a POST
  if (req.method !== "POST") {
    return res
      .status(200)
      .json({ error: "This API call only accepts POST methods" });
  }

  // get and validate body variables
  const { username, email, password } = req.body;
  console.log(req.body);
  const errorMessage = await validateForm(username, email, password);
  if (errorMessage) {
    return res.status(400).json(errorMessage);
  }

  console.log(errorMessage);
  // hash password
  const hashedPassword = await bcrypt.hash(password, 12);

  // create new User on MongoDB
  const newUser = new User({
    name: username,
    email,
    password: hashedPassword,
  });

  newUser
    .save()
    .then(async () => {
      res.status(200).json({ msg: "New User: " + newUser });
    })
    .catch((err) =>
      res.status(400).json({ error: "Error on '/api/register': " + err })
    );
}

Add custom pages for login and registration

  • Create an auth.js file in the pages directory which will render both login and register pages:
touch pages/auth.js
  • Install Formik for form handling, and Axios for routing the API requests:
yarn add axios formik
  • Paste the code below into pages/auth.js:
import { useState } from "react";
import { signIn } from "next-auth/react";
import { Field, Form, Formik } from "formik";
import axios from "axios";
import Router from "next/router";

const Auth = () => {
  const [authType, setAuthType] = useState("Login");
  const oppAuthType = {
    Login: "Register",
    Register: "Login",
  };
  const [username, setUsername] = useState("");
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  const redirectToHome = () => {
    const { pathname } = Router;
    if (pathname === "/auth") {
      Router.push("/");
    }
  };

  const registerUser = async () => {
    const res = await axios
      .post(
        "/api/register",
        { username, email, password },
        {
          headers: {
            Accept: "application/json",
            "Content-Type": "application/json",
          },
        }
      )
      .then(async () => {
        await loginUser();
        redirectToHome();
      })
      .catch((error) => {
        console.log(error);
      });
    console.log(res);
  };

  const loginUser = async () => {
    const res = await signIn("credentials", {
      redirect: false,
      email: email,
      password: password,
      callbackUrl: `${window.location.origin}`,
    });

    res.error ? console.log(res.error) : redirectToHome();
  };

  const formSubmit = (actions) => {
    actions.setSubmitting(false);

    authType === "Login" ? loginUser() : registerUser();
  };

  return (
    <>
      <div>
        <Formik
          initialValues={{}}
          validateOnChange={true}
          validateOnBlur={true}
          onSubmit={(_, actions) => {
            formSubmit(actions);
          }}
        >
          {(props) => (
            <div>
              <div>
                <h2>
                  {authType === "Register"
                    ? "Create a new account"
                    : "Login to your account"}
                </h2>
              </div>
              <Form>
                {authType === "Register" && (
                  <Field name="username">
                    {() => (
                      <div>
                        <label htmlFor="username" >
                          Username
                        </label>
                        <input
                          id="username"
                          name="username"
                          type="text"
                          value={username}
                          onChange={(e) => setUsername(e.target.value)}
                          required
                          placeholder="Username"
                        />
                      </div>
                    )}
                  </Field>
                )}
                <Field name="email">
                  {() => (
                    <div>
                      <label htmlFor="email-address">
                        Email address
                      </label>
                      <input
                        id="email-address"
                        name="email"
                        type="email"
                        autoComplete="email"
                        value={email}
                        onChange={(e) => setEmail(e.target.value)}
                        required
                        placeholder="Email address"
                      />
                    </div>
                  )}
                </Field>
                <Field name="password">
                  {() => (
                    <div>
                      <label htmlFor="password">
                        Password
                      </label>
                      <input
                        id="password"
                        name="password"
                        type="password"
                        autoComplete="current-password"
                        required
                        placeholder="Password"
                        value={password}
                        onChange={(e) => setPassword(e.target.value)}
                      />
                    </div>
                  )}
                </Field>

                <div>
                  <div>
                    <button type="button" onClick={() => setAuthType(oppAuthType[authType])}>
                      <p>
                        {" "}
                        {authType === "Login"
                          ? "Not registered yet? "
                          : "Already have an account? "}{" "}
                        {oppAuthType[authType]}
                      </p>
                    </button>
                  </div>
                </div>

                <div>
                  <button type="submit" disabled={props.isSubmitting}>
                    {authType}
                  </button>
                </div>
              </Form>
            </div>
          )}
        </Formik>
      </div>
    </>
  );
};

export default Auth;
  • Update the NextAuth config object to use our custom auth page instead of the default placeholder. Also, we will use JWT (JSON Web Tokens) for authentication:
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { MongoDBAdapter } from "@next-auth/mongodb-adapter";
import clientPromise from "../../../lib/mongodb";
import dbConnect from "../../../lib/dbConnect";
import User from "../../../models/User";
import { compare } from "bcrypt";

export default NextAuth({
  providers: [
    // Email & Password
    CredentialsProvider({
      id: "credentials",
      name: "Credentials",
      credentials: {
        email: {
          label: "Email",
          type: "text",
        },
        password: {
          label: "Password",
          type: "password",
        },
      },
      async authorize(credentials) {
        await dbConnect();

        console.log(credentials);
        // Find user with the email
        const user = await User.findOne({
          email: credentials?.email,
        });

        // Email Not found
        if (!user) {
          throw new Error("Email is not registered");
        }

        // Check hashed password with DB hashed password
        
        const isPasswordCorrect = await compare(
          credentials?.password,
          user.password
        );

        // Incorrect password
        if (!isPasswordCorrect) {
          throw new Error("Password is incorrect");
        }

        return user;
      },
    }),
  ],
  pages: {
    signIn: "/auth",
  },
  adapter: MongoDBAdapter(clientPromise),
  session: {
    strategy: "jwt",
  },
  jwt: {
    secret: process.env.NEXTAUTH_JWT_SECRET,
  },
  secret: process.env.NEXTAUTH_SECRET,
});

Integrate the Editframe SDK

In this section, we will generate a welcome video using Editframe SDK.

  • Install the @editframe/editframe-js SDK:
yarn add @editframe/editframe-js
  • Create a video.js file inside lib folder
touch lib/video.js
  • Copy and paste the code below into lib/video.js:
import "dotenv/config";
import { Editframe } from "@editframe/editframe-js";
import path from "path";
export const generateWelcomeVideo = (username) => {
  console.log(username)
  return new Promise(async (resolve, reject) => {
    try {
      const editframe = new Editframe({
        clientId: process.env.EDITFRAME_CLIENT_ID,
        token:  process.env.EDITFRAME_TOKEN
      });
      const composition = await editframe.videos.new({
        backgroundColor: "#000",
        dimensions: {
          height: 1080,
          width: 1920,
        },
        duration: 10,
      });

      await composition.addVideo(path.resolve("./welcome.mp4"), {
        size: { format: "fit" },
        timeline: {
          start: 0,
        },
        trim: {
          end: 5,
        },
      });
      await composition.addVideo(path.resolve("./happy.mp4"), {
        size: { format: "fit" },
        timeline: {
          start: 5,
        },
      });
      await composition.addText(
        {
          text: `Welcome ${username}`,
          backgroundColor: "#00000080",
          color: "#fff",
          textAlign: "center",
          textPosition: {
            x: "center",
            y: "center",
          },
        },
        {
          size: {
            height: 500,
            width: 900,
          },
          position: {
            x: "center",
            y: "center",
          },
          timeline: {
            start: 2,
          },
          trim: {
            end: 5,
          },
        }
      );

      const video = await composition.encodeSync();
      resolve(video);
    } catch (err) {
      reject(err);
    }
  });
};

Now let’s download the video files we’ll use for our video composition:

Here’s an example of a video we will send to a newly registered user:

Add a personalized voiceover to your video with Amazon Polly

Amazon Polly is a service that turns text into lifelike speech. In this tutorial, we’ll use it to add a personalized voiceover to a video that we’ll send our users via email.

  • First, install the aws-sdk package:
yarn add aws-sdk
  • Create a polly.js file inside lib directory:
touch lib/polly.js
  • In the code below, we initialize an Amazon Polly instance, store the actual text and other configuration data that we will use to generate the speech, and build a synthesizeSpeech method that will take in this custom text and return an mp3 file in a specified file path_:_
import "dotenv/config";
import AWS from "aws-sdk";
import fs from "fs";
import path from "path";

export const textToSpeech = (username) => {
  return new Promise((resolve, reject) => {
    /* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0
    ABOUT THIS NODE.JS EXAMPLE: This example works with the AWS SDK for JavaScript version 3 (v3),
    which is available at <https://github.com/aws/aws-sdk-js-v3>. This example is in the 'AWS SDK for JavaScript v3 Developer Guide' at
    <https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/polly-examples.html>.
    Purpose:
    polly.ts demonstrates how to convert text to speech using Amazon Polly,
    and automatically upload an audio file of the speech to an
    Amazon Simple Storage Service (Amazon S3) bucket.
    Inputs (replace in code):
    - BUCKET_NAME
    - IDENTITY_POOL_ID
    Running the code:
    node polly_synthesize_to_s3.js
    */
    // snippet-start:[Polly.JavaScript.general-examples.synthesizetos3_V3]
    // Example Node.js AWS Polly Script that saves an mp3 file to S3

    const Polly = new AWS.Polly({
      signatureVersion: "v4",
      region: "us-east-1",
    });

    let pollyparams = {
      Text: `<speak>Hello ${username}, <break time="0.5s"/> Welcome to the Editframe platform <break time="0.4s"/>  <amazon:breath duration="medium" volume="x-loud"/> We're happy to have you here. <break time="2s"/>   <amazon:effect name="whispered"><prosody rate="slow">we really are.</prosody></amazon:effect></speak>`,
      TextType: "ssml",
      OutputFormat: "mp3",
      VoiceId: "Amy",
    };
    Polly.synthesizeSpeech(pollyparams, (err, data) => {
      if (err) {
        reject(err.message);
        console.log(err.message);
      } else if (data) {
        if (data.AudioStream instanceof Buffer) {
          fs.writeFile(`./${username}.mp3`, data.AudioStream, function (err) {
            if (err) {
              reject(err.message);
              return console.log(err);
            }
            console.log("The file was saved!");
            resolve(path.resolve(`./${username}.mp3`));
          });
        }
      }
    });
  });
};
  • In your AWS account, create an AWS IAM user with API access to Amazon Polly, then add the following variables to your .env file:
AWS_ACCESS_KEY_ID={Your access key}
AWS_SECRET_ACCESS_KEY={Your secret access key}

Now we need to update the register.js and video.js file with a new audio file.

  • Import the textToSpeech function into the register.js file. This will return the audio file path as a promise, and pass it to the generateWelcomeVideo method.
import { textToSpeech } from "../../lib/polly";

newUser
    .save()
    .then(async () => {
      res.status(200).json({ msg: "New User: " + newUser });
      console.log({ msg: "New User: " + newUser });
      const audioFile = await textToSpeech(username);
      await generateWelcomeVideo(username, audioFile);
    })
    .catch((err) =>
      res.status(400).json({ error: "Error on '/api/register': " + err })
    );
  • In the video.js file, update the generateWelcomeVideo function by adding the audioFile path as the second parameter, and pass it to the composition.addAudio method:
export const generateWelcomeVideo = (username, audioFile) => {

    await composition.addAudio(audioFile);

    const video = await composition.encodeSync();
    resolve(video);
 }

Add an Email sender

  • Install the node-mailer package. In the root project directory, run:

    yarn add nodemailer

  • Create an email.js file inside of the lib directory:

    touch lib/email.js

    Note: In this tutorial, we will use a SendPluse free account, though you can use any other SMTP providers.

  • Copy and paste the code below inside of email.js:

import "dotenv/config";
import nodemailer from "nodemailer";
const mailer = nodemailer.createTransport({
  service: "SendPulse", // no need to set host or port etc.
  auth: {
    pass: process.env.EMAIL_PASSWORD,
    user: process.env.EMAIL_ADDRESS,
  },
});

export const sendEmail = (email, username, video) => {
  return new Promise(async (resolve, reject) => {
    const audioFile = await textToSpeech(username);
    mailer.sendMail(
      {
        from: "VERIFIED SENDPLUSE EMAIL SENDER",
        to: email,
        subject: `Welcome ${username} to our platform`,
        html: `<!DOCTYPE html> 
                <html> 
                  <body> 
                  
                  <video width="400" controls>
                    <source src="${video.streamUrl}" type="video/mp4">
                    <a href="${video.streamUrl}">
                    <img src="${video.thumbnailUrl}" />
                    </a>
                  </video>
                  
                  <p>
	                  Welcome to our platform. We're happy to have you here
                  </p>
                  
                  </body> 
                </html>`,
      },
      function (error) {
        if (error) {
          consol.log(error);
          reject(error);
        }
        resolve("done");
      }
    );
  });
};
  • Import the sendEmail function into register.js, and invoke it at the end of the then block in the newUser method. Pass the username, email and video object to sendEmail.
import { sendEmail } from "../../lib/email";

newUser
    .save()
    .then(async () => {
      res.status(200).json({ msg: "New User: " + newUser });
      console.log({ msg: "New User: " + newUser });
      const audioFile = await textToSpeech(username);
      const video = await generateWelcomeVideo(username, audioFile);
      await sendEmail(username, email, video);
    })
    .catch((err) =>
      res.status(400).json({ error: "Error on '/api/register': " + err })
    );

Add Tailwind CSS (optional)

Let’s turn to aesthetics for a moment, since our app is looking a little plain. We’ll import tailwindcss for some quick beautification.

  • Install tailwindcss postcss autoprefixer package

    yarn add tailwindcss postcss autoprefixer -D

  • Intialize tailwind css configuration file

    npx tailwindcss init -p

  • Configure your tailwindcss and purge content paths

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./pages/**/*.{js,ts,jsx,tsx}",
    "./components/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
  • Import the tailwindcss modules inside style/global.css file:
@tailwind base;
@tailwind components;
@tailwind utilities;
  • Update the pages/auth.js file with the tailwindcss styles:
import { useState } from "react";
import { useSession, signIn, getProviders } from "next-auth/react";

import { Field, Form, Formik } from "formik";
import axios from "axios";
import Router from "next/router";

const Auth = ({ providers }) => {
  const { data: session } = useSession();
  const [authType, setAuthType] = useState("Login");
  const oppAuthType = {
    Login: "Register",
    Register: "Login",
  };
  const [username, setUsername] = useState("");
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  const redirectToHome = () => {
    const { pathname } = Router;
    if (pathname === "/auth") {
      Router.push("/");
    }
  };

  const registerUser = async () => {
    const res = await axios
      .post(
        "/api/register",
        { username, email, password },
        {
          headers: {
            Accept: "application/json",
            "Content-Type": "application/json",
          },
        }
      )
      .then(async () => {
        await loginUser();
        redirectToHome();
      })
      .catch((error) => {
        console.log(error);
      });
    console.log(res);
  };

  const loginUser = async () => {
    const res = await signIn("credentials", {
      redirect: false,
      email: email,
      password: password,
      callbackUrl: `${window.location.origin}`,
    });

    res.error ? console.log(res.error) : redirectToHome();
  };

  const formSubmit = (actions) => {
    actions.setSubmitting(false);

    authType === "Login" ? loginUser() : registerUser();
  };

  return (
    <>
      <div className="flex min-h-full items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
        <Formik
          initialValues={{}}
          validateOnChange={true}
          validateOnBlur={true}
          onSubmit={(_, actions) => {
            formSubmit(actions);
          }}
          className="mt-8 space-y-6"
        >
          {(props) => (
            <div className="w-full max-w-md space-y-8">
              <div>
                <h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">
                  {authType === "Register"
                    ? "Create a new account"
                    : "Login to your account"}
                </h2>
              </div>
              <Form className="-space-y-px rounded-md shadow-sm">
                {authType === "Register" && (
                  <Field name="username">
                    {() => (
                      <div>
                        <label htmlFor="username" className="sr-only">
                          Username
                        </label>
                        <input
                          id="username"
                          name="username"
                          type="text"
                          value={username}
                          onChange={(e) => setUsername(e.target.value)}
                          required
                          className="relative block w-full appearance-none rounded-none rounded-t-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm"
                          placeholder="Username"
                        />
                      </div>
                    )}
                  </Field>
                )}
                <Field name="email">
                  {() => (
                    <div>
                      <label htmlFor="email-address" className="sr-only">
                        Email address
                      </label>
                      <input
                        id="email-address"
                        name="email"
                        type="email"
                        autoComplete="email"
                        value={email}
                        onChange={(e) => setEmail(e.target.value)}
                        required
                        className="relative block w-full appearance-none rounded-none rounded-t-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm"
                        placeholder="Email address"
                      />
                    </div>
                  )}
                </Field>
                <Field name="password">
                  {() => (
                    <div>
                      <label htmlFor="password" className="sr-only">
                        Password
                      </label>
                      <input
                        id="password"
                        name="password"
                        type="password"
                        autoComplete="current-password"
                        required
                        className="relative block w-full appearance-none rounded-none rounded-b-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm"
                        placeholder="Password"
                        value={password}
                        onChange={(e) => setPassword(e.target.value)}
                      />
                    </div>
                  )}
                </Field>

                <div className="text-sm py-4 block">
                  <div>
                    <button
                      className="font-medium text-indigo-600 hover:text-indigo-500"
                      onClick={() => setAuthType(oppAuthType[authType])}
                    >
                      <p>
                        {" "}
                        {authType === "Login"
                          ? "Not registered yet? "
                          : "Already have an account? "}{" "}
                        {oppAuthType[authType]}
                      </p>
                    </button>
                  </div>
                </div>

                <div>
                  <button
                    type="submit"
                    disabled={props.isSubmitting}
                    className="group relative flex w-full justify-center rounded-md border border-transparent bg-indigo-600 py-2 px-4 text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
                  >
                    {authType}
                  </button>
                </div>
              </Form>
            </div>
          )}
        </Formik>
      </div>
    </>
  );
};

export default Auth;
  • Update the index.js file page with the tailwindcss styles:
import { useSession, signIn, signOut } from "next-auth/react";
import Head from "next/head";

export default function Home() {
  const { data: session, status } = useSession();
  console.log(session, status);

  return (
    <div className="mx-auto max-w-7xl sm:px-6 lg:px-8">
      <Head>
        <title>
          {session ? `Welcome ${session.user.name}` : "Welcome back"}
        </title>
        <meta name="description" content="Generated by create next app" />
      </Head>
      {status === "loading" ? (
        <p>Loading..</p>
      ) : (
        <div className="md:flex md:items-center md:justify-between py-8">
          <div className="min-w-0 flex-1">
            <h2 className="text-2xl font-bold leading-7 text-dark sm:truncate sm:text-3xl sm:tracking-tight">
              {session ? <p>Welcome {session.user.name}</p> : "Welcome back"}
            </h2>
          </div>
          <div className="mt-4 flex md:mt-0 md:ml-4">
            {session ? (
              <button
                onClick={() => signOut()}
                type="button"
                className="inline-flex items-center rounded-md border border-transparent bg-gray-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-offset-gray-800"
              >
                Logout
              </button>
            ) : (
              <button
                onClick={() => signIn()}
                type="button"
                className="ml-3 inline-flex items-center rounded-md border border-transparent bg-indigo-500 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-offset-gray-800"
              >
                Sign in
              </button>
            )}
          </div>
        </div>
      )}
    </div>
  );
}

Conclusion

Et voila! We now have a Next.js app with email and password authentication that will send a welcome email to new user with a unique welcoming video. Here’s the Github repo for this project–feel free to clone, tinker, share, and let us know your feedback!

© 2024 Editframe
Making video creation easier for software developers.