├── .eslintrc.json ├── public ├── favicon.ico └── vercel.svg ├── next.config.js ├── pages ├── api │ └── hello.js ├── _app.js └── index.js ├── README.md ├── hooks └── useAuth.js ├── styles ├── globals.css └── Home.module.css ├── .gitignore ├── firebase └── index.js ├── package.json ├── api └── todo.js ├── components ├── Auth.jsx ├── AddTodo.jsx └── TodoList.jsx └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjalbinayak/medium-nextjs-firebase-todo/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | } 6 | 7 | module.exports = nextConfig 8 | -------------------------------------------------------------------------------- /pages/api/hello.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | export default function handler(req, res) { 4 | res.status(200).json({ name: 'John Doe' }) 5 | } 6 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import { ChakraProvider } from "@chakra-ui/react"; 2 | function MyApp({ Component, pageProps }) { 3 | return ( 4 | 5 | 6 | 7 | ); 8 | } 9 | 10 | export default MyApp; 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## How to create Todo App with Nextjs, Firebase and ChakraUI 2 | 3 | [Click here](https://anjalbinayak.medium.com/how-to-create-todo-app-with-nextjs-firebase-383dcee65076) to read this article 4 | 5 | [Click here](https://www.youtube.com/watch?v=wkKzKpTY4w4) to watch the explaination video 6 | 7 | Don't forget to ⭐ the repo 8 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import { Container } from "@chakra-ui/react"; 2 | import AddTodo from "../components/AddTodo"; 3 | import Auth from "../components/Auth"; 4 | import TodoList from "../components/TodoList"; 5 | 6 | export default function Home() { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /hooks/useAuth.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { auth } from "../firebase"; 3 | const useAuth = () => { 4 | const [user, setUser] = useState(null); 5 | const [isLoggedIn, setIsLoggedIn] = useState(false); 6 | 7 | useEffect(() => { 8 | auth.onAuthStateChanged((user) => { 9 | setIsLoggedIn(user && user.uid ? true : false); 10 | setUser(user); 11 | }); 12 | }); 13 | return { user, isLoggedIn }; 14 | }; 15 | 16 | export default useAuth; 17 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | @media (prefers-color-scheme: dark) { 19 | html { 20 | color-scheme: dark; 21 | } 22 | body { 23 | color: white; 24 | background: black; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /firebase/index.js: -------------------------------------------------------------------------------- 1 | import { initializeApp } from "firebase/app"; 2 | import { getAuth } from "firebase/auth"; 3 | import { getFirestore } from "firebase/firestore"; 4 | 5 | const firebaseConfig = { 6 | apiKey: "AIzaSyClH1YdssQHAA0peMtQ_wj2A4Crnc4fEgU", 7 | authDomain: "medium-firebase-next-todo.firebaseapp.com", 8 | projectId: "medium-firebase-next-todo", 9 | storageBucket: "medium-firebase-next-todo.appspot.com", 10 | messagingSenderId: "989711683222", 11 | appId: "1:989711683222:web:bc878dca5a5d251177fcb7", 12 | }; 13 | 14 | const app = initializeApp(firebaseConfig); 15 | const auth = getAuth(app); 16 | const db = getFirestore(app); 17 | 18 | export { auth, db }; 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-firebase-todo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@chakra-ui/react": "^2.2.8", 13 | "@emotion/react": "^11.10.0", 14 | "@emotion/styled": "^11.10.0", 15 | "firebase": "^9.9.3", 16 | "framer-motion": "^7.2.0", 17 | "next": "12.2.5", 18 | "react": "18.2.0", 19 | "react-dom": "18.2.0", 20 | "react-icons": "^4.4.0" 21 | }, 22 | "devDependencies": { 23 | "eslint": "8.22.0", 24 | "eslint-config-next": "12.2.5" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /api/todo.js: -------------------------------------------------------------------------------- 1 | import { db } from "../firebase"; 2 | import { 3 | collection, 4 | addDoc, 5 | updateDoc, 6 | doc, 7 | deleteDoc, 8 | } from "firebase/firestore"; 9 | 10 | const addTodo = async ({ userId, title, description, status }) => { 11 | try { 12 | await addDoc(collection(db, "todo"), { 13 | user: userId, 14 | title: title, 15 | description: description, 16 | status: status, 17 | createdAt: new Date().getTime(), 18 | }); 19 | } catch (err) {} 20 | }; 21 | 22 | const toggleTodoStatus = async ({ docId, status }) => { 23 | try { 24 | const todoRef = doc(db, "todo", docId); 25 | await updateDoc(todoRef, { 26 | status, 27 | }); 28 | } catch (err) { 29 | console.log(err); 30 | } 31 | }; 32 | 33 | const deleteTodo = async (docId) => { 34 | try { 35 | const todoRef = doc(db, "todo", docId); 36 | await deleteDoc(todoRef); 37 | } catch (err) { 38 | console.log(err); 39 | } 40 | }; 41 | 42 | export { addTodo, toggleTodoStatus, deleteTodo }; 43 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /components/Auth.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Box, Button, Link, Text, useColorMode } from "@chakra-ui/react"; 3 | import { signInWithPopup, GoogleAuthProvider } from "firebase/auth"; 4 | import { FaGoogle, FaMoon, FaSun } from "react-icons/fa"; 5 | import { auth } from "../firebase"; 6 | import useAuth from "../hooks/useAuth"; 7 | const Auth = () => { 8 | const { toggleColorMode, colorMode } = useColorMode(); 9 | const { isLoggedIn, user } = useAuth(); 10 | 11 | const handleAuth = async () => { 12 | const provider = new GoogleAuthProvider(); 13 | signInWithPopup(auth, provider) 14 | .then((result) => { 15 | // This gives you a Google Access Token. You can use it to access the Google API. 16 | const credential = GoogleAuthProvider.credentialFromResult(result); 17 | const token = credential.accessToken; 18 | // The signed-in user info. 19 | const user = result.user; 20 | // ... 21 | }) 22 | .catch((error) => { 23 | // Handle Errors here. 24 | const errorCode = error.code; 25 | const errorMessage = error.message; 26 | // The email of the user's account used. 27 | const email = error.customData.email; 28 | // The AuthCredential type that was used. 29 | const credential = GoogleAuthProvider.credentialFromError(error); 30 | // ... 31 | }); 32 | }; 33 | 34 | return ( 35 | 36 | {" "} 39 | {isLoggedIn && ( 40 | <> 41 | {user.email} 42 | auth.signOut()}> 43 | Logout 44 | 45 | 46 | )} 47 | {!isLoggedIn && ( 48 | 51 | )} 52 | 53 | ); 54 | }; 55 | 56 | export default Auth; 57 | -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | } 4 | 5 | .main { 6 | min-height: 100vh; 7 | padding: 4rem 0; 8 | flex: 1; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | .footer { 16 | display: flex; 17 | flex: 1; 18 | padding: 2rem 0; 19 | border-top: 1px solid #eaeaea; 20 | justify-content: center; 21 | align-items: center; 22 | } 23 | 24 | .footer a { 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | flex-grow: 1; 29 | } 30 | 31 | .title a { 32 | color: #0070f3; 33 | text-decoration: none; 34 | } 35 | 36 | .title a:hover, 37 | .title a:focus, 38 | .title a:active { 39 | text-decoration: underline; 40 | } 41 | 42 | .title { 43 | margin: 0; 44 | line-height: 1.15; 45 | font-size: 4rem; 46 | } 47 | 48 | .title, 49 | .description { 50 | text-align: center; 51 | } 52 | 53 | .description { 54 | margin: 4rem 0; 55 | line-height: 1.5; 56 | font-size: 1.5rem; 57 | } 58 | 59 | .code { 60 | background: #fafafa; 61 | border-radius: 5px; 62 | padding: 0.75rem; 63 | font-size: 1.1rem; 64 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 65 | Bitstream Vera Sans Mono, Courier New, monospace; 66 | } 67 | 68 | .grid { 69 | display: flex; 70 | align-items: center; 71 | justify-content: center; 72 | flex-wrap: wrap; 73 | max-width: 800px; 74 | } 75 | 76 | .card { 77 | margin: 1rem; 78 | padding: 1.5rem; 79 | text-align: left; 80 | color: inherit; 81 | text-decoration: none; 82 | border: 1px solid #eaeaea; 83 | border-radius: 10px; 84 | transition: color 0.15s ease, border-color 0.15s ease; 85 | max-width: 300px; 86 | } 87 | 88 | .card:hover, 89 | .card:focus, 90 | .card:active { 91 | color: #0070f3; 92 | border-color: #0070f3; 93 | } 94 | 95 | .card h2 { 96 | margin: 0 0 1rem 0; 97 | font-size: 1.5rem; 98 | } 99 | 100 | .card p { 101 | margin: 0; 102 | font-size: 1.25rem; 103 | line-height: 1.5; 104 | } 105 | 106 | .logo { 107 | height: 1em; 108 | margin-left: 0.5rem; 109 | } 110 | 111 | @media (max-width: 600px) { 112 | .grid { 113 | width: 100%; 114 | flex-direction: column; 115 | } 116 | } 117 | 118 | @media (prefers-color-scheme: dark) { 119 | .card, 120 | .footer { 121 | border-color: #222; 122 | } 123 | .code { 124 | background: #111; 125 | } 126 | .logo img { 127 | filter: invert(1); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /components/AddTodo.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Box, 4 | Input, 5 | Button, 6 | Textarea, 7 | Stack, 8 | Select, 9 | useToast, 10 | } from "@chakra-ui/react"; 11 | import useAuth from "../hooks/useAuth"; 12 | import { addTodo } from "../api/todo"; 13 | const AddTodo = () => { 14 | const [title, setTitle] = React.useState(""); 15 | const [description, setDescription] = React.useState(""); 16 | const [status, setStatus] = React.useState("pending"); 17 | const [isLoading, setIsLoading] = React.useState(false); 18 | 19 | const toast = useToast(); 20 | 21 | const { isLoggedIn, user } = useAuth(); 22 | 23 | const handleTodoCreate = async () => { 24 | if (!isLoggedIn) { 25 | toast({ 26 | title: "You must be logged in to create a todo", 27 | status: "error", 28 | duration: 9000, 29 | isClosable: true, 30 | }); 31 | return; 32 | } 33 | setIsLoading(true); 34 | const todo = { 35 | title, 36 | description, 37 | status, 38 | userId: user.uid, 39 | }; 40 | await addTodo(todo); 41 | setIsLoading(false); 42 | 43 | setTitle(""); 44 | setDescription(""); 45 | setStatus("pending"); 46 | 47 | toast({ title: "Todo created successfully", status: "success" }); 48 | }; 49 | 50 | return ( 51 | 52 | 53 | setTitle(e.target.value)} 57 | /> 58 | 59 |