├── .babelrc ├── .eslintrc.json ├── .github └── FUNDING.yml ├── .gitignore ├── .prettierrc.js ├── LICENSE ├── README.md ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── css │ └── homestyle.css └── image │ └── built-with-appwrite.svg ├── src ├── api │ └── api.js ├── components │ ├── Login.js │ └── SignUp.js ├── hooks │ └── index.js ├── pages │ ├── _app.js │ ├── _document.js │ └── index.js └── utils │ └── config.js ├── tailwind.config.js ├── test └── setup.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "next/babel" 4 | ], 5 | "plugins": [ 6 | ["styled-components", { "minify": false }] 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "browser": true, 5 | "es6": true, 6 | "jest/globals": true 7 | }, 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:react/recommended", 11 | "plugin:prettier/recommended", 12 | "prettier/react" 13 | ], 14 | "globals": { 15 | "Atomics": "readonly", 16 | "SharedArrayBuffer": "readonly" 17 | }, 18 | "parserOptions": { 19 | "ecmaFeatures": { 20 | "jsx": true 21 | }, 22 | "ecmaVersion": 2020, 23 | "sourceType": "module" 24 | }, 25 | "plugins": [ 26 | "jest", 27 | "react" 28 | ], 29 | "settings": { 30 | "import/resolver": { 31 | "babel-module": {} 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [ Sooraj-s-98 ] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: soorajs 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # misc 7 | .DS_Store 8 | .env.local 9 | .env.development.local 10 | .env.test.local 11 | .env.production.local 12 | 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | 17 | # Text Editors 18 | .vscode 19 | 20 | # Next.js 21 | .next 22 | 23 | .now -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: "es5", 3 | semi: true, 4 | singleQuote: true 5 | }; 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 sooraj s 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🔖 Todo With NextJS 2 | 3 | ![logo]( public/image/built-with-appwrite.svg "Logo") 4 | A simple todo app built with Appwrite and Nextjs 5 | 6 | 7 | 8 | 9 | ## 🎬 Getting Started 10 | 11 | ### 🤘 Install Appwrite 12 | Follow our simple [Installation Guide](https://appwrite.io/docs/installation) to get Appwrite up and running in no time. You can either deploy Appwrite on your local machine or, on any cloud provider of your choice. 13 | 14 | > Note: If you setup Appwrite on your local machine, you will need to create a public IP so that your hosted frontend can access it. 15 | 16 | 17 | 1. Add a new Web App in Appwrite and enter the endpoint of your website (`localhost, .vercel.app etc`) 18 | ![Create Web App](https://user-images.githubusercontent.com/52352285/135745293-7fc105a9-631c-41b7-83b9-04aae7810bd5.png) 19 | 20 | 2. Create a new collection with the following properties 21 | * **Rules** 22 | 23 | Add the following rules to the collection. 24 | > Make sure that your key exactly matches the key in the images 25 | 26 |

27 | Content Rule 28 |

29 | 30 |

31 | IsComplete Rule 32 |

33 | 34 | * **Permissions** 35 | 36 | Add the following permissions to your collections. These permissions ensure that only registered users can access the collection. 37 | 38 |

39 | Collection Permissions 40 |

41 | 42 | ### 🚀 Deploy the Front End 43 | You have two options to deploy the front-end and we will cover both of them here. In either case, you will need to fill in these environment variables that help your frontend connect to Appwrite. 44 | 45 | * REACT_APP_ENDPOINT - Your Appwrite endpoint 46 | * REACT_APP_PROJECT - Your Appwrite project ID 47 | * REACT_APP_COLLECTION_ID - Your Appwrite collection ID 48 | 49 | 50 | ### **Run locally** 51 | 52 | Follow these instructions to run the demo app locally 53 | 54 | ```sh 55 | $ git clone https://github.com/Sooraj-s-98/appwrite-todo-with-nextjs 56 | $ cd appwrite-todo-with-nextjs 57 | ``` 58 | 59 | 60 | Now fill your environment variables 61 | 62 | ![image](https://user-images.githubusercontent.com/52352285/135744323-a4b1a948-4011-4a39-abcd-63b58853555e.png) 63 | 64 | 65 | Now run the following commands and you should be good to go 💪🏼 66 | 67 | ``` 68 | $ npm install 69 | $ npm run dev 70 | ``` 71 | 72 | ## Screenshot 73 | 74 | ![image](https://user-images.githubusercontent.com/52352285/135747618-c65c983c-6f7f-4870-94b5-63d455fbd817.png) 75 | 76 | 77 | ![image](https://user-images.githubusercontent.com/52352285/135747863-3c804f3f-54a7-4d91-92f6-0d4eb32ac61b.png) 78 | 79 | 80 | ## Thanks 81 | 82 | Glad to see here! Show some love by [starring](https://github.com/Sooraj-s-98/appwrite-todo-with-nextjs) this repository. 83 | 84 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | future: { 3 | webpack5: true, 4 | }, 5 | env: { 6 | REACT_APP_ENDPOINT: 'http://localhost:4000/v1', 7 | REACT_APP_PROJECT: '61592488544e4', 8 | REACT_APP_COLLECTION_ID: '615925aa9e94f', 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-todo-with-nextjs", 3 | "version": "0.0.1", 4 | "license": "SEE LICENSE IN LICENSE FILE", 5 | "scripts": { 6 | "dev": "next", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint-staged": "npx lint-staged", 10 | "lint": "npx eslint ./src" 11 | }, 12 | "jest": { 13 | "setupFilesAfterEnv": [ 14 | "/test/setup.js" 15 | ] 16 | }, 17 | "dependencies": { 18 | "@material-ui/core": "^4.10.2", 19 | "appwrite": "^3.0.1", 20 | "bootstrap": "^5.1.1", 21 | "next": "^9.4.4", 22 | "prop-types": "^15.7.2", 23 | "react": "^16.13.1", 24 | "react-bootstrap": "^1.6.4", 25 | "react-dom": "^16.13.1", 26 | "styled-components": "^5.1.1" 27 | }, 28 | "devDependencies": { 29 | "babel-plugin-styled-components": "^1.10.7", 30 | "enzyme": "^3.11.0", 31 | "enzyme-adapter-react-16": "^1.15.2", 32 | "eslint": "^7.2.0", 33 | "eslint-config-prettier": "^6.11.0", 34 | "eslint-import-resolver-babel-module": "^5.1.2", 35 | "eslint-plugin-jest": "^23.7.0", 36 | "eslint-plugin-prettier": "^3.1.4", 37 | "eslint-plugin-react": "^7.20.0", 38 | "husky": "^4.2.5", 39 | "jest": "^26.0.1", 40 | "lint-staged": "^10.2.11", 41 | "prettier": "^2.0.5" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/css/homestyle.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /public/image/built-with-appwrite.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/api/api.js: -------------------------------------------------------------------------------- 1 | import { Appwrite } from "appwrite"; 2 | import { Server } from "../utils/config"; 3 | 4 | let api = { 5 | sdk: null, 6 | 7 | provider: () => { 8 | if (api.sdk) { 9 | return api.sdk; 10 | } 11 | let appwrite = new Appwrite(); 12 | appwrite.setEndpoint(Server.endpoint).setProject(Server.project); 13 | api.sdk = appwrite; 14 | return appwrite; 15 | }, 16 | 17 | createAccount: (email, password, name) => { 18 | return api.provider().account.create(email, password, name); 19 | }, 20 | 21 | getAccount: () => { 22 | return api.provider().account.get(); 23 | }, 24 | 25 | createSession: (email, password) => { 26 | return api.provider().account.createSession(email, password); 27 | }, 28 | 29 | deleteCurrentSession: () => { 30 | return api.provider().account.deleteSession('current'); 31 | }, 32 | 33 | createDocument: (collectionId, data, read, write) => { 34 | return api 35 | .provider() 36 | .database.createDocument(collectionId, data, read, write); 37 | }, 38 | 39 | listDocuments: (collectionId) => { 40 | return api.provider().database.listDocuments(collectionId); 41 | }, 42 | 43 | updateDocument: (collectionId, documentId, data, read, write) => { 44 | return api 45 | .provider() 46 | .database.updateDocument(collectionId, documentId, data, read, write); 47 | }, 48 | 49 | deleteDocument: (collectionId, documentId) => { 50 | return api.provider().database.deleteDocument(collectionId, documentId); 51 | }, 52 | }; 53 | 54 | export default api; 55 | -------------------------------------------------------------------------------- /src/components/Login.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import api from "../api/api"; 3 | import SignUp from "./SignUp"; 4 | import { FetchState } from "../hooks"; 5 | 6 | const Login = ({ dispatch }) => { 7 | const [email, setEmail] = useState(); 8 | const [password, setPassword] = useState(); 9 | const [register, setRegister] = useState(false); 10 | 11 | const handleLogin = async (e) => { 12 | e.preventDefault(); 13 | dispatch({ type: FetchState.FETCH_INIT }); 14 | try { 15 | await api.createSession(email, password); 16 | const data = await api.getAccount(); 17 | dispatch({ type: FetchState.FETCH_SUCCESS, payload: data }); 18 | } catch (e) { 19 | dispatch({ type: FetchState.FETCH_FAILURE }); 20 | } 21 | }; 22 | 23 | return register ? ( 24 | 25 | ) : ( 26 | <> 27 | 28 |

Login

29 |

30 | {" "} 31 | Don't have an account ?{" "} 32 | setRegister(true)} 35 | > 36 | Sign Up 37 | {" "} 38 |

39 | 40 | 41 |
42 |
43 |
44 | 45 | setEmail(e.target.value)} 51 | /> 52 | We'll never share your email with anyone else. 53 |
54 |
55 | 56 | setPassword(e.target.value)} 61 | /> 62 |
63 | 70 | 71 |
72 |
73 | 74 | 75 | 76 | ); 77 | }; 78 | 79 | export default Login; 80 | -------------------------------------------------------------------------------- /src/components/SignUp.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import api from "../api/api"; 3 | import { FetchState } from "../hooks"; 4 | 5 | const SignUp = ({ setRegister, dispatch }) => { 6 | const [name, setName] = useState(); 7 | const [email, setEmail] = useState(); 8 | const [password, setPassword] = useState(); 9 | 10 | const handleSignup = async (e) => { 11 | e.preventDefault(); 12 | dispatch({ type: FetchState.FETCH_INIT }); 13 | try { 14 | const user = await api.createAccount(email, password, name); 15 | await api.createSession(email, password); 16 | dispatch({ type: FetchState.FETCH_SUCCESS, payload: user }); 17 | } catch (e) { 18 | dispatch({ type: FetchState.FETCH_FAILURE }); 19 | } 20 | }; 21 | 22 | return ( 23 | <> 24 | 25 | 26 | 27 |

Sign Up

28 |

29 | {" "} 30 | Already have an account ?{" "} 31 | setRegister(false)} 34 | > 35 | Login 36 | {" "} 37 |

38 | 39 |
40 |
41 | 42 | setName(e.target.value)} 47 | /> 48 |
49 |
50 | 51 | setEmail(e.target.value)} 57 | /> 58 | We'll never share your email with anyone else. 59 |
60 |
61 | 62 | setPassword(e.target.value)} 67 | /> 68 |
69 | 77 |
78 | 79 | ); 80 | }; 81 | 82 | export default SignUp; 83 | -------------------------------------------------------------------------------- /src/hooks/index.js: -------------------------------------------------------------------------------- 1 | import api from '../api/api'; 2 | import { useEffect, useReducer } from 'react'; 3 | 4 | export const FetchState = { 5 | FETCH_INIT: 0, 6 | FETCH_SUCCESS: 1, 7 | FETCH_FAILURE: 2, 8 | }; 9 | 10 | export const useGetTodos = (stale) => { 11 | const reducer = (state, action) => { 12 | switch (action.type) { 13 | case FetchState.FETCH_INIT: 14 | return { ...state, isLoading: true, isError: false }; 15 | case FetchState.FETCH_SUCCESS: 16 | return { 17 | ...state, 18 | isLoading: false, 19 | isError: false, 20 | todos: action.payload, 21 | }; 22 | case FetchState.FETCH_FAILURE: 23 | return { ...state, isLoading: false, isError: true }; 24 | default: 25 | throw new Error(); 26 | } 27 | }; 28 | 29 | const [state, dispatch] = useReducer(reducer, { 30 | isLoading: false, 31 | isError: false, 32 | todos: [], 33 | }); 34 | 35 | useEffect(() => { 36 | let didCancel = false; 37 | const getTodos = async () => { 38 | dispatch({ type: FetchState.FETCH_INIT }); 39 | try { 40 | const data = await api.listDocuments( 41 | process.env.REACT_APP_COLLECTION_ID 42 | ); 43 | if (!didCancel) { 44 | dispatch({ type: FetchState.FETCH_SUCCESS, payload: data.documents }); 45 | } 46 | } catch (e) { 47 | if (!didCancel) { 48 | dispatch({ type: FetchState.FETCH_FAILURE }); 49 | } 50 | } 51 | }; 52 | getTodos(); 53 | return () => (didCancel = true); 54 | }, [stale]); 55 | 56 | return [state]; 57 | }; 58 | 59 | export const useGetUser = () => { 60 | const reducer = (state, action) => { 61 | switch (action.type) { 62 | case FetchState.FETCH_INIT: 63 | return { ...state, isLoading: true, isError: false }; 64 | case FetchState.FETCH_SUCCESS: 65 | return { 66 | ...state, 67 | isLoading: false, 68 | isError: false, 69 | user: action.payload, 70 | }; 71 | case FetchState.FETCH_FAILURE: 72 | return { ...state, isLoading: false, isError: true }; 73 | default: 74 | throw new Error(); 75 | } 76 | }; 77 | 78 | const [state, dispatch] = useReducer(reducer, { 79 | isLoading: false, 80 | isError: true, 81 | data: [], 82 | }); 83 | 84 | useEffect(() => { 85 | let didCancel = false; 86 | const getTodos = async () => { 87 | dispatch({ type: FetchState.FETCH_INIT }); 88 | try { 89 | const account = await api.getAccount(); 90 | if (!didCancel) { 91 | dispatch({ type: FetchState.FETCH_SUCCESS, payload: account }); 92 | } 93 | } catch (e) { 94 | if (!didCancel) { 95 | dispatch({ type: FetchState.FETCH_FAILURE }); 96 | } 97 | } 98 | }; 99 | getTodos(); 100 | return () => (didCancel = true); 101 | }, []); 102 | 103 | return [state, dispatch]; 104 | }; 105 | -------------------------------------------------------------------------------- /src/pages/_app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import App from 'next/app'; 3 | import Head from 'next/head'; 4 | import 'tailwindcss/tailwind.css' 5 | import '../../public/css/homestyle.css' 6 | 7 | export default class MyApp extends App { 8 | componentDidMount() { 9 | const jssStyles = document.querySelector('#jss-server-side'); 10 | if (jssStyles) { 11 | jssStyles.parentElement.removeChild(jssStyles); 12 | } 13 | } 14 | 15 | render() { 16 | const { Component, pageProps } = this.props; 17 | 18 | return ( 19 | <> 20 | 21 | Appwrite NextJs 22 | 26 | 27 | 32 | 33 | 34 | 35 | 36 | 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/pages/_document.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Document from 'next/document'; 3 | import { ServerStyleSheets as MaterialUiServerStyleSheets } from '@material-ui/styles'; 4 | import { ServerStyleSheet as StyledComponentSheets } from 'styled-components'; 5 | 6 | class MyDocument extends Document { 7 | static async getInitialProps(ctx) { 8 | const styledComponentSheet = new StyledComponentSheets(); 9 | const materialUiSheets = new MaterialUiServerStyleSheets(); 10 | const originalRenderPage = ctx.renderPage; 11 | 12 | try { 13 | ctx.renderPage = () => 14 | originalRenderPage({ 15 | enhanceApp: (App) => (props) => 16 | materialUiSheets.collect( 17 | styledComponentSheet.collectStyles() 18 | ), 19 | }); 20 | 21 | const initialProps = await Document.getInitialProps(ctx); 22 | 23 | return { 24 | ...initialProps, 25 | styles: ( 26 | <> 27 | {initialProps.styles} 28 | {materialUiSheets.getStyleElement()} 29 | {styledComponentSheet.getStyleElement()} 30 | 31 | ), 32 | }; 33 | } finally { 34 | styledComponentSheet.seal(); 35 | } 36 | } 37 | } 38 | 39 | export default MyDocument; 40 | -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Login from '../components/Login'; 3 | import { useGetUser } from "../hooks"; 4 | import 'bootstrap/dist/css/bootstrap.min.css'; 5 | const Index = () => { 6 | 7 | const [{ user, isLoading, isError }, dispatch] = useGetUser(); 8 | return ( 9 | <> 10 |
11 | 12 | 13 |
14 | 15 | ); 16 | }; 17 | 18 | export default Index; 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/utils/config.js: -------------------------------------------------------------------------------- 1 | export const Server = { 2 | endpoint: process.env.REACT_APP_ENDPOINT, 3 | project: process.env.REACT_APP_PROJECT, 4 | collectionID: process.env.REACT_APP_COLLECTION_ID, 5 | }; 6 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: [], 3 | purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'], 4 | darkMode: false, // or 'media' or 'class' 5 | theme: { 6 | extend: {}, 7 | }, 8 | variants: { 9 | extend: {}, 10 | }, 11 | plugins: [], 12 | } 13 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | 4 | configure({ adapter: new Adapter() }); 5 | --------------------------------------------------------------------------------