Building a Color Pallet Manager using NextJS + Tailwind CSS + Supabase.io - Part-1

Welcome to Part 1 of Building a Color Pallet Manager using NextJS, Tailwind CSS and Supabase.io

What we will be building

A Simple color pallet manager web project with help of NextJS and Tailwind CSS and Supabase.io as our Backend service for data store.

Project Source and Preview

What does it includes?

  1. Users can login with magic links sent to their emails
  2. Logged in users can create multiple projects to their account
  3. Users can add multiple pallets and multiple colors to pallet
  4. Colors can be sorted in each pallet from light to dark or dark to light luminosity value
  5. and Finally, each pallet colors can be exported to Tailwind CSS Color configuration, Sass variables and CSS Variables as well.

Get started with Coding

1. Setup Next JS project

yarn create next-app my-app

2. Add Tailwind CSS

yarn add tailwindcss@latest postcss@latest autoprefixer@latest
  1. Initialize the tailwind config
npx tailwindcss init -p
  1. We will update Purge configuration for tailwind by adding below to our tailwind.config.js
purge: [
    './src/pages/**/*.{js,ts,jsx,tsx}',
    './src/layouts/**/*.{js,ts,jsx,tsx}',
    './src/components/**/*.{js,ts,jsx,tsx}',
  ],
  1. We will create new file at src/styles/app.css and will add below css to it which will compile to tailwind css when we build.
@tailwind base;
@tailwind components;
@tailwind utilities;

.logoIcon svg {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
}

.bgGradiants {
  background-image: linear-gradient(212deg, #1eae98, #a9f1df, #233e8b, #e93b81);
  background-size: 800% 800%;

  -webkit-animation: bgGradiantAnomation 30s ease infinite;
  -moz-animation: bgGradiantAnomation 30s ease infinite;
  -o-animation: bgGradiantAnomation 30s ease infinite;
  animation: bgGradiantAnomation 30s ease infinite;
  transition: all 0.3s;
}

.bgGradiants:hover {
  background-image: linear-gradient(120deg, #233e8b, #e93b81, #1eae98, #a9f1df);
}

.bgGradiants.delay500 {
  animation-delay: 0.5s;
}

@-webkit-keyframes bgGradiantAnomation {
  0% {
    background-position: 91% 0%;
  }
  50% {
    background-position: 10% 100%;
  }
  100% {
    background-position: 91% 0%;
  }
}
@-moz-keyframes bgGradiantAnomation {
  0% {
    background-position: 91% 0%;
  }
  50% {
    background-position: 10% 100%;
  }
  100% {
    background-position: 91% 0%;
  }
}
@-o-keyframes bgGradiantAnomation {
  0% {
    background-position: 91% 0%;
  }
  50% {
    background-position: 10% 100%;
  }
  100% {
    background-position: 91% 0%;
  }
}
@keyframes bgGradiantAnomation {
  0% {
    background-position: 91% 0%;
  }
  50% {
    background-position: 10% 100%;
  }
  100% {
    background-position: 91% 0%;
  }
}

3. Setup Supabase.io

  1. Setup a Supabase.io account here
  2. Once your account is setup, you can create new project in supabase.io and create a table for storing our projects.
  3. You can import the SQL from our source here to Supabase SQL section for quickly creating the table with all permissions.
  1. We will add supabase client to src/libs/clients/supabase.js. We also need to install the library.
yarn add @supabase/supabase-js
// src/libs/clients/supabase.js

import { createClient } from '@supabase/supabase-js'

export const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
)

The value for NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY is copied from the Supabase Dashboard for project.

https://app.supabase.io/project/[YourProjectUniqueID]/settings/api

4. Arrange Project folder structure

  1. We will re arrange our next js project boilerplate with below folders under src directory.
    Project Directory structure

  2. We will add jsconfig.json at the root of the project for allowing absolute imports.

{
  "compilerOptions": {
    "baseUrl": "./src",
  }
}
  1. We will add next.config.js with a small customization for webpack for using SVG as components.
module.exports = {
  webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
    config.module.rules.push({
      test: /\.svg$/,
      use: ['@svgr/webpack'],
    });
    return config;
  },
};

5. Lets Start Coding ;)

  1. We will create a simple AppContext using React Context.
//src/context/AppContext

export const AppContext = createContext({ pallets: [] });

export const AppContextProvider = ({ children, initialData }) => {
  const [state, dispatch] = useReducer(reducer, initialData);
  return (
    <AppContext.Provider value={{ state, dispatch }}>
      {children}
    </AppContext.Provider>
  );
};
  1. We will create src/pages/_app.js for the importing our global css and setting up the context provider.
//src/pages/_app.js

import { AppContextProvider } from 'context/AppContext';
import '../styles/app.css';

const MyApp = ({ Component, pageProps }) => {
  let pallets = [];
  return (
    <AppContextProvider initialData={pallets}>
      <Component {...pageProps} />
    </AppContextProvider>
  );
};

export default MyApp;
  1. We will create a layout file for our pages. src/layouts/MainLayout.js
//src/layouts/MainLayout.js

import PropTypes from 'prop-types';
import Header from 'components/Header';
import SeoComponent from 'components/SeoComponent';
import Footer from 'components/Footer';
import ToasterNotification from 'components/ToasterNotification';
import ErrorBoundary from 'components/ErrorBoundary';

const MainLayout = ({ seoData, children, showPalletForm, onAddNewPallet }) => {
  return (
    <div className="min-h-screen flex flex-col pt-40 md:pt-20">
      <SeoComponent data={seoData} />
      <Header showPalletForm={showPalletForm} onAddNewPallet={onAddNewPallet} />
      <div className="flex flex-1">{children}</div>
      <Footer />
      <ErrorBoundary>
        <ToasterNotification />
      </ErrorBoundary>
    </div>
  );
};

MainLayout.defaultProps = {
  showPalletForm: true,
  seoData: {},
  children: '',
  onAddNewPallet: () => {},
};
MainLayout.propTypes = {
  seoData: PropTypes.object,
  children: PropTypes.node,
  showPalletForm: PropTypes.bool,
  onAddNewPallet: PropTypes.func,
};

export default MainLayout;
  1. We will create our homepage under src/pages/index.js
// src/pages/index.js

const HomePage = () => {
return (
    <MainLayout seoData={seoData} onAddNewPallet={onAddNewPallet}>
      ... We will add our components here
    </MainLayout>
  );
};

export default HomePage;

6. Building components

We will create all our components inside src/components folder. Since there are lot of components, to simplify I have added the repo source here, so that you can clone and see each individual components. I will explain about the functionality within the components.

  1. In our header component, we will call the custom useAuth hook which we will create and show the login form when user click on the Login button.
// src/components/Header/index.js

import PropTypes from 'prop-types';
import Container from 'components/Container';
import AddNewPallet from 'components/Forms/AddNewPallet';
import Logo from 'components/Logo';
import HeaderButtons from './HeaderButtons';

const Header = ({ showPalletForm, onAddNewPallet }) => {
  return (
    <div className="py-2 shadow-xl fixed top-0 z-1000 w-full left-0 right-0 bg-white">
      <Container>
        <div className="flex justify-between flex-col md:flex-row">
          <Logo />
          {showPalletForm && (
            <div className="w-full flex-1">
              <AddNewPallet onSubmit={onAddNewPallet} />
            </div>
          )}
          <HeaderButtons />
        </div>
      </Container>
    </div>
  );
};

Header.defaultProps = {
  showPalletForm: true,
};

Header.propTypes = {
  showPalletForm: PropTypes.bool,
};

export default Header;
  1. Our useAuth hook will look like this. It uses supabase client which we created before.
// src/hooks/useAuth.js

import { useState, useEffect } from 'react';
import { supabase } from 'libs/clients/supabase';

export const useAuth = () => {
  const [loading, setLoading] = useState(true);
  const [session, setSession] = useState(null);
  useEffect(() => {
    setSession(supabase.auth.session());
    supabase.auth.onAuthStateChange((_event, session) => {
      setSession(session);
    });
    setLoading(false);
  }, []);
  const logout = () => supabase.auth.signOut();
  const isLoggedIn = session?.user?.id || false;
  return { session, logout, isLoggedIn, loading };
};
  1. We will initiate our Login form using the Header button commponent
// src/components/Header/HeaderButtons.js

import { Fragment, useState } from 'react';
import PropTypes from 'prop-types';
import LoginForm from 'components/Auth/LoginForm';
import { useAuth } from 'hooks';
import Link from 'next/link';

const HeaderButtons = () => {
  const [showLogin, setShowLogin] = useState(false);
  const { session, logout } = useAuth();
  const isLoggedIn = session?.user?.id || false;
  const toggleLogin = () => setShowLogin(!showLogin);

  return (
    <div className="block w-auto text-center py-2">
      {isLoggedIn ? (
        <Fragment>
          <Link href="/">
            <a className="bg-white text-xs font-semibold text-theme-primary-500 mx-2">
              My Projects
            </a>
          </Link>
          <button
            type="button"
            onClick={logout}
            className="appearance-none bg-white text-xs font-semibold text-theme-secondary-500 mx-2"
          >
            Logout
          </button>
        </Fragment>
      ) : (
        <button
          onClick={toggleLogin}
          className="appearance-none bgGradiants p-2 px-4 inline-block rounded-md text-sm font-semibold text-white mx-1 shadow-lg"
        >
          Login
        </button>
      )}
      {showLogin && (
        <div className="fixed top-0 left-0 right-0 bottom-0 z-1000 bg-theme-light-blue-900 bg-opacity-30 w-full h-full flex justify-center align-middle items-center">
          <LoginForm onSuccess={toggleLogin} />
          <div
            className="absolute w-full z-100 h-full left-0 top-0 right-0 bottom-0"
            onClick={toggleLogin}
          />
        </div>
      )}
      <a
        className="bg-white text-xs font-semibold text-theme-primary-500 mx-2"
        href="https://github.com/abdulkader/color-pallet-manager"
        target="_blank"
      >
        <img
          src="/GitHub-Mark-64px.png"
          alt="Github"
          className="w-6 md:w-8 inline-block"
        />
      </a>
    </div>
  );
};

HeaderButtons.defaultProps = {
  onSave: () => {},
};

HeaderButtons.propTypes = {
  onSave: PropTypes.func,
};

export default HeaderButtons;
  1. Our Login form component will look like below which simply collect email and call the supabase client for login via email.
// src/components/Auth/LoginForm.js

import { Fragment, useState } from 'react';
import PropTypes from 'prop-types';
import Button from 'components/Button';
import { supabase } from 'libs/clients/supabase';
import { addToast } from 'libs/utilities';

const LoginForm = ({ onSuccess }) => {
  const [email, setEmail] = useState('');
  const handleChange = (e) => {
    setEmail(e.target.value);
  };

  const handleLogin = async () => {
    try {
      const { error } = await supabase.auth.signIn({ email });
      if (error) throw error;
      addToast('Check your email for the login link!');
      onSuccess();
    } catch (error) {
      addToast(error.error_description || error.message, 'error');
    }
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    handleLogin();
  };
  return (
    <div className="sm:max-w-lg w-full p-10 bg-white rounded-xl z-10 mx-auto shadow-2xl z-900">
      <div className="text-center">
        <h2 className="text-2xl font-semibold text-center block text-transparent bg-clip-text bgGradiants">
          Get Magic Link
        </h2>
        <p className="mt-2 text-sm text-gray-400 p-4 px-8">
          You can use the magic link to login and manage your color pallets
        </p>
      </div>
      <form
        method="post"
        onSubmit={handleSubmit}
        className="relative flex flex-col justify-start align-middle items-center"
      >
        <Fragment>
          <input
            type="text"
            name="pallet"
            id="pallet"
            value={email}
            onChange={handleChange}
            maxLength="20"
            className="appearance-none w-full block outline-none focus:outline-none p-1 text-sm h-8 border border-gray-200"
            placeholder="Enter your email"
          />
          <Button
            type="submit"
            className="bgGradiants rounded-md text-sm font-semibold text-white mx-1 shadow-lg px-4 my-2"
            label="Send me magic link"
          />
        </Fragment>
      </form>
    </div>
  );
};

LoginForm.propTypes = {
  onSuccess: PropTypes.func.isRequired,
};

export default LoginForm;

... to be continued

38