38
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
A Simple color pallet manager web project with help of NextJS and Tailwind CSS and Supabase.io as our Backend service for data store.
- 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.
yarn create next-app my-app
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%;
}
}
- 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
{
"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;
},
};
- 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;
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 usessupabase
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
38