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?
  • Users can login with magic links sent to their emails
  • Logged in users can create multiple projects to their account
  • Users can add multiple pallets and multiple colors to pallet
  • Colors can be sorted in each pallet from light to dark or dark to light luminosity value
  • 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
  • Initialize the tailwind config
  • npx tailwindcss init -p
  • 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}',
      ],
  • 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
  • Setup a Supabase.io account here
  • Once your account is setup, you can create new project in supabase.io and create a table for storing our projects.
  • You can import the SQL from our source here to Supabase SQL section for quickly creating the table with all permissions.
  • 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
  • We will re arrange our next js project boilerplate with below folders under src directory.
    Project Directory structure

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

  • {
      "compilerOptions": {
        "baseUrl": "./src",
      }
    }
  • 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 ;)
  • 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>
      );
    };
  • 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;
  • 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;
  • 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.
  • 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;
  • 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 };
    };
  • 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;
  • 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

    47

    This website collects cookies to deliver better user experience

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