├── .eslintrc.json ├── .gitignore ├── README.md ├── firebase └── clientApp.ts ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.tsx ├── add-todo.tsx ├── api │ └── hello.ts └── index.tsx ├── public ├── favicon.ico └── vercel.svg ├── styles ├── Home.module.css └── globals.css └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.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 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /firebase/clientApp.ts: -------------------------------------------------------------------------------- 1 | import {initializeApp} from "firebase/app"; 2 | import {getFirestore} from "firebase/firestore"; 3 | 4 | initializeApp({ 5 | apiKey:process.env.NEXT_PUBLIC_FIREBASE_API_KEY, 6 | authDomain:process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, 7 | projectId:process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, 8 | storageBucket:process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET, 9 | messagingSenderId:process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, 10 | appId:process.env.NEXT_PUBLIC_FIREBASE_APP_ID, 11 | measurementId:process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID 12 | }); 13 | 14 | const firestore = getFirestore(); 15 | 16 | export {firestore}; -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/basic-features/typescript for more information. 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | reactStrictMode: true, 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todos-app-1", 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 | "firebase": "^9.1.2", 13 | "next": "11.1.2", 14 | "react": "17.0.2", 15 | "react-dom": "17.0.2" 16 | }, 17 | "devDependencies": { 18 | "@types/react": "17.0.27", 19 | "eslint": "8.0.0", 20 | "eslint-config-next": "11.1.2", 21 | "typescript": "4.4.3" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | import type { AppProps } from 'next/app' 3 | 4 | function MyApp({ Component, pageProps }: AppProps) { 5 | return 6 | } 7 | export default MyApp 8 | -------------------------------------------------------------------------------- /pages/add-todo.tsx: -------------------------------------------------------------------------------- 1 | import { doc } from '@firebase/firestore'; 2 | import { setDoc } from 'firebase/firestore'; 3 | import type { NextPage } from 'next' 4 | import Head from "next/head"; 5 | import { useState } from 'react'; 6 | import { firestore } from '../firebase/clientApp'; 7 | import styles from '../styles/Home.module.css' 8 | 9 | const AddTodo:NextPage = () => { 10 | 11 | const [title,setTitle] = useState(""); // title 12 | const [description,setDescription] = useState("");// description 13 | const [error,setError] = useState("");// error 14 | const [message,setMessage] = useState("");// message 15 | 16 | const handleSubmit = (e: { preventDefault: () => void; }) => { 17 | e.preventDefault(); // avoid default behaviour 18 | 19 | if(!title || !description){ // check for any null value 20 | return setError("All fields are required"); 21 | } 22 | addTodo(); 23 | } 24 | 25 | const addTodo = async () => { 26 | // get the current timestamp 27 | const timestamp:string = Date.now().toString(); 28 | // create a pointer to our document 29 | const _todo = doc(firestore,`todos/${timestamp}`); 30 | // structure the todo data 31 | const todoData = { 32 | title, 33 | description, 34 | done:false 35 | }; 36 | 37 | try{ 38 | //add the document 39 | await setDoc(_todo,todoData); 40 | //show a success message 41 | setMessage("Todo added successfully"); 42 | //reset fields 43 | setTitle(""); 44 | setDescription(""); 45 | }catch(error){ 46 | //show an error message 47 | setError("An error occurred while adding todo"); 48 | } 49 | }; 50 | 51 | return ( 52 | 53 | 54 | Add todo 55 | 56 | 57 | 58 | 59 | 60 | 61 | Add todo 62 | 63 | 64 | 65 | 66 | { 67 | error ? ( 68 | 69 | {error} 70 | 71 | ) : null 72 | } 73 | 74 | { 75 | message ? ( 76 | 77 | 78 | {message}. Proceed to Home 79 | 80 | 81 | ) : null 82 | } 83 | 84 | 85 | Title 86 | setTitle(e.target.value)} 89 | value={title} 90 | /> 91 | 92 | 93 | 94 | Description 95 | setDescription(e.target.value)} 98 | value={description} 99 | /> 100 | 101 | 102 | 103 | Submit 104 | 105 | 106 | 107 | 108 | 109 | 110 | ) 111 | } 112 | 113 | export default AddTodo; -------------------------------------------------------------------------------- /pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next' 2 | import Head from 'next/head' 3 | import { firestore } from '../firebase/clientApp' 4 | import { collection, deleteDoc, doc, DocumentData, getDocs, limit, query, QueryDocumentSnapshot, updateDoc, where } from "@firebase/firestore"; 5 | import styles from '../styles/Home.module.css' 6 | import { useEffect, useState } from 'react'; 7 | 8 | const Home: NextPage = () => { 9 | 10 | const [todos,setTodos] = useState[]>([]); 11 | const [loading,setLoading] = useState(true); 12 | 13 | useEffect( () => { 14 | getTodos(); 15 | setTimeout( () => { 16 | setLoading(false); 17 | },2000) 18 | 19 | },[]); 20 | 21 | 22 | const todosCollection = collection(firestore,'todos'); 23 | 24 | const getTodos = async () => { 25 | const todosQuery = query(todosCollection,where('done','==',false),limit(10)); 26 | const querySnapshot = await getDocs(todosQuery); 27 | const result: QueryDocumentSnapshot[] = []; 28 | querySnapshot.forEach((snapshot) => { 29 | result.push(snapshot); 30 | }) 31 | setTodos(result); 32 | }; 33 | 34 | const updateTodo = async (documentId: string) => { 35 | // create a pointer to the document id 36 | const _todo = doc(firestore,`todos/${documentId}`); 37 | 38 | // update the doc by setting done to true 39 | await updateDoc(_todo,{ 40 | "done":true 41 | }); 42 | 43 | // retrieve todos 44 | getTodos(); 45 | } 46 | 47 | const deleteTodo = async (documentId:string) => { 48 | // create a pointer to the document id 49 | const _todo = doc(firestore,`todos/${documentId}`); 50 | 51 | // delete the doc 52 | await deleteDoc(_todo); 53 | 54 | // retrieve todos 55 | getTodos(); 56 | } 57 | 58 | return ( 59 | 60 | 61 | Todos app 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | Todos app 70 | 71 | 72 | 73 | { 74 | loading ? ( 75 | 76 | Loading 77 | 78 | ): 79 | todos.length === 0 ? ( 80 | 81 | No undone todos 82 | Consider adding a todo from here 83 | 84 | ) : ( 85 | todos.map((todo,index) => { 86 | return ( 87 | 88 | 89 | {todo.data().title} 90 | {todo.data().description} 91 | 92 | 93 | 94 | updateTodo(todo.id)}>Mark as done 95 | 96 | deleteTodo(todo.id)}>Delete 97 | 98 | 99 | 100 | 101 | ) 102 | }) 103 | ) 104 | } 105 | 106 | 107 | 108 | 109 | 117 | 118 | ) 119 | } 120 | 121 | export default Home 122 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rose-stack/full-stack-nextjs-with-typescript-and-firebase-database/aad6a66bdafa47c14f0b53409034d145781c2682/public/favicon.ico -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | min-height: 100vh; 3 | padding: 0 0.5rem; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | } 9 | 10 | .main { 11 | padding: 1rem 0; 12 | flex: 1; 13 | display: flex; 14 | flex-direction: column; 15 | align-items: center; 16 | width: 100%; 17 | } 18 | 19 | .footer { 20 | width: 100%; 21 | height: 100px; 22 | border-top: 1px solid #eaeaea; 23 | display: flex; 24 | justify-content: center; 25 | align-items: center; 26 | } 27 | 28 | .footer a { 29 | display: flex; 30 | justify-content: center; 31 | align-items: center; 32 | flex-grow: 1; 33 | } 34 | 35 | .title a { 36 | color: #0070f3; 37 | text-decoration: none; 38 | } 39 | 40 | .title a:hover, 41 | .title a:focus, 42 | .title a:active { 43 | text-decoration: underline; 44 | } 45 | 46 | .title { 47 | margin: 0; 48 | line-height: 1.15; 49 | font-size: 1.5rem; 50 | } 51 | 52 | .title, 53 | .description { 54 | text-align: center; 55 | } 56 | 57 | .description { 58 | line-height: 1.5; 59 | font-size: 1.5rem; 60 | } 61 | 62 | .grid { 63 | display: flex; 64 | flex-wrap: wrap; 65 | width: 80%; 66 | margin-top: 1rem; 67 | } 68 | 69 | .card { 70 | margin: 1rem auto; 71 | padding: 0.5rem; 72 | text-align: left; 73 | color: inherit; 74 | text-decoration: none; 75 | border: 1px solid #eaeaea; 76 | border-radius: 10px; 77 | transition: color 0.15s ease, border-color 0.15s ease; 78 | width: 60%; 79 | } 80 | 81 | .card h2 { 82 | margin: 0 0 1rem 0; 83 | font-size: 1.5rem; 84 | } 85 | 86 | .card p { 87 | margin: 0; 88 | font-size: 1.25rem; 89 | line-height: 1.5; 90 | } 91 | 92 | .cardActions{ 93 | width: 100%; 94 | display: flex; 95 | justify-content: space-between; 96 | margin-top: 7px; 97 | } 98 | 99 | .form{ 100 | width:50%; 101 | margin: 1rem auto; 102 | padding: 10px; 103 | box-shadow: 0 1px 3px 0 #d4d4d5, 0 0 0 1px #d4d4d5; 104 | } 105 | 106 | .formGroup { 107 | width: 100%; 108 | margin: 1rem 0px; 109 | } 110 | 111 | .formGroup label{ 112 | display: block; 113 | margin-bottom: 0.5rem; 114 | } 115 | 116 | .formGroup input[type="text"]{ 117 | width:100%; 118 | padding: 10px; 119 | } 120 | 121 | .formGroup textarea{ 122 | width: 100%; 123 | padding: 10px; 124 | } 125 | 126 | .error { 127 | color: red; 128 | text-align: center; 129 | } 130 | 131 | .success{ 132 | color: green; 133 | text-align: center; 134 | } 135 | 136 | .success a{ 137 | color: blue; 138 | text-decoration: underline; 139 | } 140 | 141 | @media (max-width: 600px) { 142 | .grid { 143 | width: 100%; 144 | flex-direction: column; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve" 16 | }, 17 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 18 | "exclude": ["node_modules"] 19 | } 20 | --------------------------------------------------------------------------------
{error}
78 | {message}. Proceed to Home 79 |
Consider adding a todo from here
{todo.data().description}