Create a To-Do List App with Next JS and Tailwind
To be productive and to remember all the tasks for the day, the To-Do app is very important. In this article, you will see the whole process of creating a to-do app from scratch. This to-do list app includes functionalities like adding new task, editing the task and deleting the task by clicking on the buttons available.
Output Preview: Let us have a look at how the final output will look like
Prerequisites:
Approach to create a To-Do List App:
Through this app you will be able to add new tasks, change ones you’ve already made, and delete tasks you don’t need anymore. It’s as easy as clicking a button! Plus, we’ll make sure your tasks stay saved, in the local storage.
So, in this project there is separate components like the Header.js which will act like a Navigation bar for the to-do app, then in the index.js file there is the form to add the to-do. There is a separate component to view the todos and that is todos.js.
Steps to create the Project and Install required modules
Step 1: Create the NextJs App using the following command.
npx create-next-app@latest
Configuration which you should follow while creating App :
Step 2: Move to the project folder from Terminal
cd <project_name>
Step 3: Install tailwind
npm i tailwind
Project Structure:
The updated dependencies in package.json file will look like:
"dependencies": {
"next": "14.1.0",
"react": "^18",
"react-dom": "^18",
"tailwind": "^4.0.0"
},
"devDependencies": {
"autoprefixer": "^10.0.1",
"postcss": "^8",
"tailwindcss": "^3.3.0"
}
Step 4: Write the following code in different files(The name of the files is mentioned in the first line of each code block)
Javascript
// pages/index.js import { useState } from 'react' export default function Home() { const [todo, setTodo] = useState({ title: "" , desc: "" }) const addTodo = () => { let todos = localStorage.getItem( "todos" ) if (todos) { let todosJson = JSON.parse(todos) if (todosJson.filter( value => { return value.title == todo.title } ).length > 0) { alert( "Todo with this title already exists" ) } else { todosJson.push(todo) localStorage .setItem( "todos" , JSON.stringify(todosJson)) alert( "Todo has been added" ) setTodo({ title: "" , desc: "" }) } } else { localStorage.setItem( "todos" , JSON.stringify([todo])) } } const onChange = (e) => { setTodo( { ...todo, [e.target.name]: e.target.value } ) console.log(todo) } return ( <div className= " text-3xl" > <section className= "text-gray-600 body-font" > <div className= "container px-5 py-24 mx-auto flex flex-wrap items-center" > <div className= "rounded-lg p-8 flex flex-col md:ml-auto w-full mt-10 md:mt-0 bg-slate-100 " > <h2 className= "text-gray-900 text-lg font-medium title-font mb-5" > Add a Todo </h2> <div className= "relative mb-4" > <label for = "title" className= "leading-7 text-sm text-gray-600" > Todo Title </label> <input onChange={onChange} value={todo.title} type= "text" id= "title" name= "title" className= "w-full bg-white rounded border border-gray-300 focus:border-green-800 focus:ring-2 focus:ring-green-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out" autoComplete= 'false' /> </div> <div className= "relative mb-4" > <label for = "desc" className= "leading-7 text-sm text-gray-600" > Todo Description</label> <input onChange={onChange} value={todo.desc} type= "text" id= "desc" name= "desc" className= "w-full bg-white rounded border border-gray-300 focus:border-green-800 focus:ring-2 focus:ring-green-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out" autoComplete= 'false' /> </div> <button onClick={addTodo} className= "text-white bg-green-800 border-0 py-2 px-8 focus:outline-none w-fit hover:bg-green-600 rounded text-lg" >Add Todo</button> </div> </div> </section> </div> ) } |
Javascript
// _app.js import Header from '@/components/Header' import '@/styles/globals.css' export default function App({ Component, pageProps }) { return ( <> <Header /> <div className= "container mx-auto min-h-screen" > <Component {...pageProps} /> </div> </> ) } |
Javascript
// pages/todos.js import React, { useEffect, useState } from "react" ; const todos = () => { const [todos, setTodos] = useState([]); useEffect(() => { let todos = localStorage.getItem( "todos" ); if (!todos) { localStorage.setItem( "todos" , JSON.stringify([])); } else { setTodos(JSON.parse(todos)); } }, []); const deleteTodo = (title) => { let newTodos = todos.filter((item) => { return item.title != title; }); localStorage.setItem( "todos" , JSON.stringify(newTodos)); setTodos(newTodos); }; return ( <section className= "text-gray-600 body-font" > <div className= "container px-5 py-24 mx-auto" > <div className= "flex flex-col text-center w-full mb-20" > <h1 className= "text-4xl font-medium title-font mb-4 text-gray-900" > Your Todos </h1> {todos.length == 0 && ( <p className= "mx-auto leading-relaxed text-base" > Please add a todo by going to the homepage </p> )} </div> { todos.length > 0 && ( <div className= "w-full" > <div className= "relative overflow-x-auto shadow-md sm:rounded-lg" > <table className= "w-full text-sm text-left rtl:text-right text-gray-500 " > <thead className= "text-xs text-gray-700 uppercase bg-gray-50 " > <tr> <th scope= "col" className= "px-6 py-3" > Title </th> <th scope= "col" className= "px-6 py-3" > Description </th> <th scope= "col" className= "px-6 py-3" > Action </th> </tr> </thead> <tbody> {todos && todos.map((item, index) => { return ( <tr className= "odd:bg-white even:bg-gray-50 border-b " key={index}> <th scope= "row" className= "px-6 py-4 font-medium text-gray-900 whitespace-nowrap" > {item.title} </th> <td className= "px-6 py-4" >{item.desc}</td> <td className= "px-6 py-4" > <span className= "inline-flex" > <a className= " cursor-pointer font-medium border-2 border-red-500 rounded-md p-1 hover:bg-red-500 hover:text-white" onClick={() => { deleteTodo(item.title); }} > <svg xmlns= "http://www.w3.org/2000/svg" viewBox= "0 0 30 30" width= "20px" height= "20px" > { " " } <path d= "M 14.984375 2.4863281 A 1.0001 1.0001 0 0 0 14 3.5 L 14 4 L 8.5 4 A 1.0001 1.0001 0 0 0 7.4863281 5 L 6 5 A 1.0001 1.0001 0 1 0 6 7 L 24 7 A 1.0001 1.0001 0 1 0 24 5 L 22.513672 5 A 1.0001 1.0001 0 0 0 21.5 4 L 16 4 L 16 3.5 A 1.0001 1.0001 0 0 0 14.984375 2.4863281 z M 6 9 L 7.7929688 24.234375 C 7.9109687 25.241375 8.7633438 26 9.7773438 26 L 20.222656 26 C 21.236656 26 22.088031 25.241375 22.207031 24.234375 L 24 9 L 6 9 z" /> </svg> </a> <a className= "ml-2 cursor-pointer border-2 border-green-500 rounded-md p-1 hover:bg-green-500 hover:text-white" href={`/edit/${item.title}`} > <svg xmlns= "http://www.w3.org/2000/svg" viewBox= "0 0 50 50" width= "20px" height= "20px" > <path d= "M 43.125 2 C 41.878906 2 40.636719 2.488281 39.6875 3.4375 L 38.875 4.25 L 45.75 11.125 C 45.746094 11.128906 46.5625 10.3125 46.5625 10.3125 C 48.464844 8.410156 48.460938 5.335938 46.5625 3.4375 C 45.609375 2.488281 44.371094 2 43.125 2 Z M 37.34375 6.03125 C 37.117188 6.0625 36.90625 6.175781 36.75 6.34375 L 4.3125 38.8125 C 4.183594 38.929688 4.085938 39.082031 4.03125 39.25 L 2.03125 46.75 C 1.941406 47.09375 2.042969 47.457031 2.292969 47.707031 C 2.542969 47.957031 2.90625 48.058594 3.25 47.96875 L 10.75 45.96875 C 10.917969 45.914063 11.070313 45.816406 11.1875 45.6875 L 43.65625 13.25 C 44.054688 12.863281 44.058594 12.226563 43.671875 11.828125 C 43.285156 11.429688 42.648438 11.425781 42.25 11.8125 L 9.96875 44.09375 L 5.90625 40.03125 L 38.1875 7.75 C 38.488281 7.460938 38.578125 7.011719 38.410156 6.628906 C 38.242188 6.246094 37.855469 6.007813 37.4375 6.03125 C 37.40625 6.03125 37.375 6.03125 37.34375 6.03125 Z" /> </svg> </a> </span> </td> </tr> ); })} </tbody> </table> </div> </div> ) } </div> </section> ); }; export default todos; |
Javascript
// pages/_document.js import { Html, Head, Main, NextScript } from 'next/document' export default function Document() { return ( <Html lang= "en" > <Head /> <body> <Main /> <NextScript /> </body> </Html> ) } |
Javascript
// pages/edit/[title].js import React from 'react' import { useRouter } from 'next/router' import { useEffect, useState } from 'react' const Edit = () => { const router = useRouter() const { title } = router.query const [todo, setTodo] = useState({ title: "" , desc: "" }) const updateTodo = () => { let todos = localStorage.getItem( "todos" ) if (todos) { let todosJson = JSON.parse(todos) if (todosJson.filter(value => { return value.title == title }).length > 0) { let index = todosJson.findIndex(value => { return value.title == title }) todosJson[index].title = todo.title todosJson[index].desc = todo.desc localStorage.setItem( "todos" , JSON.stringify(todosJson)) alert( "Todo has been updated" ) } else { alert( "Todo does not exist" ) } } else { localStorage.setItem( "todos" , JSON.stringify([todo])) } } useEffect(() => { let todos = localStorage.getItem( "todos" ) if (todos) { let todosJson = JSON.parse(todos) let ftodo = todosJson.filter(e => title == e.title) console.log(ftodo) if (ftodo.length > 0) { setTodo(ftodo[0]) } } }, [router.isReady]) const onChange = (e) => { setTodo({ ...todo, [e.target.name]: e.target.value }) console.log(todo) } return ( <div className= "my-2 text-3xl" > <section className= "text-gray-600 body-font" > <div className= "container px-5 py-24 mx-auto flex flex-wrap items-center" > <div className= "bg-gray-100 rounded-lg p-8 flex flex-col md:ml-auto w-full mt-10 md:mt-0" > <h2 className= "text-gray-900 text-lg font-medium title-font mb-5" > Update a Todo </h2> <div className= "relative mb-4" > <label for = "title" className= "leading-7 text-sm text-gray-600" > Todo Title</label> <input onChange={onChange} value={todo.title} type= "text" id= "title" name= "title" className= "w-full bg-white rounded border border-gray-300 focus:border-green-500 focus:ring-2 focus:ring-indigo-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out" /> </div> <div className= "relative mb-4" > <label for = "desc" className= "leading-7 text-sm text-gray-600" > Todo Text </label> <input onChange={onChange} value={todo.desc} type= "text" id= "desc" name= "desc" className= "w-full bg-white rounded border border-gray-300 focus:border-green-500 focus:ring-2 focus:ring-indigo-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out" /> </div> <button onClick={updateTodo} className= "text-white bg-green-500 border-0 py-2 px-8 focus:outline-none w-fit hover:bg-green-600 rounded text-lg" >Update Todo</button> <p className= "text-xs text-gray-500 mt-3" > The best todo list app out there! </p> </div> </div> </section> </div> ) } export default Edit |
Javascript
// components/Header.js import Link from 'next/link' import React from 'react' const Header = () => { return ( <header className= "text-gray-600 body-font" > <div className= "container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center" > <h1 className= "flex title-font font-medium items-center text-gray-900 mb-4 md:mb-0" > <span className= "ml-3 text-2xl text-green-800" >GfG </span> <span className= 'text-black' >TodoList</span> </h1> <nav className= "md:ml-auto flex flex-wrap items-center text-base justify-center" > <Link href={ "/" } className= "mr-5 hover:text-gray-900" >Add ➕</Link> <Link href={ "/todos" } className= "mr-5 hover:text-white hover:bg-green-800 border-2 p-2 text-black border-green-800 rounded " >My Todos ???? </Link> </nav> </div> <hr className= "border-t-2 border-gray-200" /> </header> ) } export default Header |
CSS
/*global.css */ @tailwind base; @tailwind components; @tailwind utilities; |
Run your app by executing below command.
npm run dev