├── .env.example ├── .github └── ISSUE_TEMPLATE │ ├── bug.yaml │ ├── documentation.yaml │ └── feature.yaml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── README.md ├── components ├── alert.tsx └── todoItem.tsx ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.tsx ├── api │ └── todos.ts ├── index.tsx ├── login.tsx ├── signup.tsx └── todos.tsx ├── postcss.config.js ├── public ├── assets │ ├── appwrite.svg │ ├── delete.svg │ ├── github.svg │ ├── message (1).txt │ ├── message.txt │ ├── nextjs.svg │ ├── svelte.svg │ └── twitter.svg └── favicon.ico ├── store ├── global.tsx └── types.tsx ├── styles └── globals.css ├── tailwind.config.js ├── tsconfig.json └── utils └── jwt.ts /.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_ENDPOINT="" 2 | REACT_APP_PROJECT="" 3 | REACT_APP_COLLECTION_ID="" 4 | REACT_APP_DATABASE_ID="" -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yaml: -------------------------------------------------------------------------------- 1 | name: "🐛 Bug Report" 2 | description: "Submit a bug report to help us improve" 3 | title: "🐛 Bug Report: " 4 | labels: [bug] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out our bug report form 🙏 10 | - type: textarea 11 | id: steps-to-reproduce 12 | validations: 13 | required: true 14 | attributes: 15 | label: "👟 Reproduction steps" 16 | description: "How do you trigger this bug? Please walk us through it step by step." 17 | placeholder: "When I ..." 18 | - type: textarea 19 | id: expected-behavior 20 | validations: 21 | required: true 22 | attributes: 23 | label: "👍 Expected behavior" 24 | description: "What did you think would happen?" 25 | placeholder: "It should ..." 26 | - type: textarea 27 | id: actual-behavior 28 | validations: 29 | required: true 30 | attributes: 31 | label: "👎 Actual Behavior" 32 | description: "What did actually happen? Add screenshots, if applicable." 33 | placeholder: "It actually ..." 34 | - type: dropdown 35 | id: appwrite-version 36 | attributes: 37 | label: "🎲 Appwrite version" 38 | description: "What version of Appwrite are you running?" 39 | options: 40 | - Version 0.10.x 41 | - Version 0.9.x 42 | - Version 0.8.x 43 | - Version 0.7.x 44 | - Version 0.6.x 45 | - Different version (specify in environment) 46 | validations: 47 | required: true 48 | - type: dropdown 49 | id: operating-system 50 | attributes: 51 | label: "💻 Operating system" 52 | description: "What OS is your server / device running on?" 53 | options: 54 | - Linux 55 | - MacOS 56 | - Windows 57 | - Something else 58 | validations: 59 | required: true 60 | - type: textarea 61 | id: enviromnemt 62 | validations: 63 | required: false 64 | attributes: 65 | label: "🧱 Your Environment" 66 | description: "Is your environment customized in any way?" 67 | placeholder: "I use Cloudflare for ..." 68 | - type: checkboxes 69 | id: no-duplicate-issues 70 | attributes: 71 | label: "👀 Have you spent some time to check if this issue has been raised before?" 72 | description: "Have you Googled for a similar issue or checked our older issues for a similar bug?" 73 | options: 74 | - label: "I checked and didn't find similar issue" 75 | required: true 76 | - type: checkboxes 77 | id: read-code-of-conduct 78 | attributes: 79 | label: "🏢 Have you read the Code of Conduct?" 80 | options: 81 | - label: "I have read the [Code of Conduct](https://github.com/appwrite/appwrite/blob/HEAD/CODE_OF_CONDUCT.md)" 82 | required: true 83 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.yaml: -------------------------------------------------------------------------------- 1 | name: "📚 Documentation" 2 | description: "Report an issue related to documentation" 3 | title: "📚 Documentation: " 4 | labels: [documentation] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to make our documentation better 🙏 10 | - type: textarea 11 | id: issue-description 12 | validations: 13 | required: true 14 | attributes: 15 | label: "💭 Description" 16 | description: "A clear and concise description of what the issue is." 17 | placeholder: "Documentation should not ..." 18 | - type: checkboxes 19 | id: no-duplicate-issues 20 | attributes: 21 | label: "👀 Have you spent some time to check if this issue has been raised before?" 22 | description: "Have you Googled for a similar issue or checked our older issues for a similar bug?" 23 | options: 24 | - label: "I checked and didn't find similar issue" 25 | required: true 26 | - type: checkboxes 27 | id: read-code-of-conduct 28 | attributes: 29 | label: "🏢 Have you read the Code of Conduct?" 30 | options: 31 | - label: "I have read the [Code of Conduct](https://github.com/appwrite/appwrite/blob/HEAD/CODE_OF_CONDUCT.md)" 32 | required: true 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.yaml: -------------------------------------------------------------------------------- 1 | name: 🚀 Feature 2 | description: "Submit a proposal for a new feature" 3 | title: "🚀 Feature: " 4 | labels: [feature] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out our feature request form 🙏 10 | - type: textarea 11 | id: feature-description 12 | validations: 13 | required: true 14 | attributes: 15 | label: "🔖 Feature description" 16 | description: "A clear and concise description of what the feature is." 17 | placeholder: "You should add ..." 18 | - type: textarea 19 | id: pitch 20 | validations: 21 | required: true 22 | attributes: 23 | label: "🎤 Pitch" 24 | description: "Please explain why this feature should be implemented and how it would be used. Add examples, if applicable." 25 | placeholder: "In my use-case, ..." 26 | - type: checkboxes 27 | id: no-duplicate-issues 28 | attributes: 29 | label: "👀 Have you spent some time to check if this issue has been raised before?" 30 | description: "Have you Googled for a similar issue or checked our older issues for a similar bug?" 31 | options: 32 | - label: "I checked and didn't find similar issue" 33 | required: true 34 | - type: checkboxes 35 | id: read-code-of-conduct 36 | attributes: 37 | label: "🏢 Have you read the Code of Conduct?" 38 | options: 39 | - label: "I have read the [Code of Conduct](https://github.com/appwrite/appwrite/blob/HEAD/CODE_OF_CONDUCT.md)" 40 | required: true 41 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity, expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at team@appwrite.io. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🔖 Todo With Next.js 2 | 3 | ![Easter Eggs Claimed](https://img.shields.io/github/issues-pr-closed-raw/appwrite/todo-with-vue/easter-egg?label=Easter%20Eggs%20Claimed&style=flat-square&logo=data:image/png;base64,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) 4 | 5 | A simple todo app built with Appwrite and Next.js. 6 | This repository serves the purpose of demonstrating SSR and JWT usage in a Next.js application. 7 | 8 | If you simply want to try out the App, go ahead and check out the demo at https://appwrite-todo-with-nextjs.vercel.app/ 9 | 10 | ## 🎬 Getting Started 11 | 12 | ### 🤘 Install Appwrite 13 | 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. 14 | 15 | > 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. 16 | 17 | We need to make a few configuration changes to your Appwrite server. 18 | 19 | 1. Add a new Web App in Appwrite and enter the endpoint of your website (`localhost, .vercel.app etc`) 20 | ![Create Web App](https://user-images.githubusercontent.com/20852629/113019434-3c27c900-919f-11eb-997c-1da5a8303ceb.png) 21 | 22 | 2. Create a new collection with the following properties 23 | * **Rules** 24 | Add the following rules to the collection. 25 | > Make sure that your key exactly matches the key in the images 26 | 27 |

28 | Content Rule 29 |

30 | 31 |

32 | IsComplete Rule 33 |

34 | 35 | * **Permissions** 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 | * REACT_APP_DATABASE_ID - Your Appwrite database ID 49 | 50 | ### **Deploy to a Static Hosting Provider** 51 | 52 | Use the following buttons to deploy to your favourite hosting provider in one click! We support Vercel for Next.js. You will need to enter the environment variables above when prompted. 53 | 54 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fappwrite%2Ftodo-with-nextjs&env=REACT_APP_ENDPOINT,REACT_APP_PROJECT,REACT_APP_COLLECTION_ID&envDescription=Your%20Appwrite%20Endpoint%2C%20Project%20ID%20and%20Collection%20ID%20) 55 | 56 | ### **Run locally** 57 | 58 | Follow these instructions to run the demo app locally 59 | 60 | ```sh 61 | $ git clone https://github.com/appwrite/todo-with-nextjs 62 | $ cd todo-with-nextjs 63 | ``` 64 | 65 | Run the following command to generate your `.env` vars 66 | 67 | ```sh 68 | $ cp .env.example .env 69 | ``` 70 | 71 | Now fill in the envrionment variables we discussed above in your `.env` 72 | 73 | Now run the following commands and you should be good to go 💪🏼 74 | ``` 75 | $ npm install 76 | $ npm run dev 77 | ``` 78 | 79 | ## 🤕 Support 80 | If you get stuck anywhere, hop onto one of our [support channels in discord](https://appwrite.io/discord) and we'd be delighted to help you out 🤝 81 | 82 | 83 | ## 😧 Help Wanted 84 | Our access credentials were recently compromised and someone tried to ruin these demos. They decided to leave behind 15 easter eggs 🥚 for you to discover. If you find them, submit a PR cleaning up that section of the code (One PR per person across all the repos). You can track the number of claimed Easter Eggs using the badge at the top. 85 | 86 | The first 15 people to get their PRs merged will receive some Appwrite Swags 🤩 . Just head over to our [Discord channel](https://appwrite.io/discord) and share your PR link with us. 87 | 88 | > *UPDATE **17-11-2021**:* The easter egg contest is now closed. 89 | -------------------------------------------------------------------------------- /components/alert.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | const Alert = ({ message }) => { 4 | const [showAlert, setShowAlert] = useState(true); 5 | const TIMEOUT = 3000; 6 | 7 | useEffect(() => { 8 | let timeout = setTimeout(() => setShowAlert(false), TIMEOUT); 9 | return () => { 10 | clearTimeout(timeout); 11 | }; 12 | }, [message]); 13 | 14 | return ( 15 | <> 16 | {showAlert && ( 17 |
20 | 21 | 22 | 23 | 24 | {message} 25 | 26 | 34 |
35 | )} 36 | 37 | ); 38 | }; 39 | 40 | export default Alert; 41 | -------------------------------------------------------------------------------- /components/todoItem.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useRecoilState } from "recoil"; 3 | import { todoState, userState } from "../store/global"; 4 | import { Todo } from "../store/types"; 5 | import { getJWT } from "../utils/jwt"; 6 | 7 | const TodoItem = ({ item }) => { 8 | const [todo, setTodo] = useState(item); 9 | const [todos, setTodos] = useRecoilState(todoState); 10 | const [user, setUser] = useRecoilState(userState); 11 | 12 | const toggle = async (item) => { 13 | const jwt = await getJWT(); 14 | await fetch('/api/todos', { 15 | method: 'PATCH', 16 | headers: { 17 | JWT: jwt 18 | }, 19 | body: JSON.stringify({ 20 | user: user.$id, 21 | todo: { 22 | $id: todo.$id, 23 | isComplete: !todo.isComplete 24 | } 25 | }) 26 | }); 27 | setTodo({ 28 | ...todo, 29 | isComplete: !todo.isComplete 30 | }); 31 | }; 32 | 33 | const handleDelete = async () => { 34 | const jwt = await getJWT(); 35 | await fetch('/api/todos', { 36 | method: 'DELETE', 37 | headers: { 38 | JWT: jwt 39 | }, 40 | body: JSON.stringify({ 41 | user: user.$id, 42 | todo: { 43 | $id: todo.$id, 44 | } 45 | }) 46 | }); 47 | setTodos(todos.filter(n => n.$id !== todo.$id)) 48 | }; 49 | 50 | return ( 51 |
  • 52 |
    53 | 59 |
    63 | {todo["content"]} 64 |
    65 |
    66 | 72 |
  • 73 | ); 74 | }; 75 | 76 | export default TodoItem; -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | future: { 3 | webpack5: true, 4 | }, 5 | env: { 6 | REACT_APP_ENDPOINT: "https://demo.appwrite.io/v1", 7 | REACT_APP_PROJECT: "6062f9c2c09ce", 8 | REACT_APP_COLLECTION_ID: "60631b201eec4", 9 | REACT_APP_DATABASE_ID: "default" 10 | } 11 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo-with-nextjs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "appwrite": "^9.0.0", 12 | "next": "^10.2.0", 13 | "react": "17.0.2", 14 | "react-dom": "17.0.2", 15 | "recoil": "^0.3.1" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^14.14.37", 19 | "@types/react": "^17.0.3", 20 | "autoprefixer": "^10.2.5", 21 | "postcss": "^8.2.10", 22 | "tailwindcss": "^2.1.1", 23 | "typescript": "^4.2.4" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "tailwindcss/tailwind.css"; 2 | import { AppProps } from "next/app"; 3 | import { RecoilRoot } from "recoil"; 4 | 5 | const App = ({ Component, pageProps }: AppProps) => 6 | 7 | export default App; -------------------------------------------------------------------------------- /pages/api/todos.ts: -------------------------------------------------------------------------------- 1 | import { AppwriteException } from 'appwrite'; 2 | import type { NextApiRequest, NextApiResponse } from 'next'; 3 | import type { User, Todo } from "../../store/types"; 4 | import { appwrite, Server } from '../../store/global'; 5 | 6 | 7 | export default async (req: NextApiRequest, res: NextApiResponse) => { 8 | const { jwt } = req.headers 9 | const payload = JSON.parse(req.body || null); 10 | 11 | if (!jwt) { 12 | return; 13 | } 14 | 15 | appwrite.account.client.setJWT(jwt.toString()); 16 | 17 | try { 18 | switch (req.method.toUpperCase()) { 19 | case "GET": 20 | res.status(200).json(await appwrite.database.listDocuments(Server.collectionID)); 21 | break; 22 | 23 | case "POST": 24 | res.status(200).json(await appwrite.database.createDocument(Server.collectionID, 'unique()', payload.todo, [`user:${payload.user}`], [`user:${payload.user}`])); 25 | break; 26 | 27 | case "PATCH": 28 | res.status(200).json(await appwrite.database.updateDocument(Server.collectionID, payload.todo.$id, payload.todo, [`user:${payload.user}`], [`user:${payload.user}`])) 29 | break; 30 | 31 | case "DELETE": 32 | res.status(200).json(await appwrite.database.deleteDocument(Server.collectionID, payload.todo.$id)); 33 | break; 34 | 35 | default: 36 | res.send(true) 37 | break; 38 | } 39 | } catch (error) { 40 | const e: AppwriteException = error; 41 | console.log(e.message) 42 | res.status(401).json(e); 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { useEffect } from "react"; 3 | import { useRecoilState } from "recoil"; 4 | import { appwrite, userState } from "../store/global"; 5 | import { User } from "../store/types"; 6 | 7 | const links = [ 8 | { 9 | href: "http://github.com/appwrite/appwrite", 10 | icon: , 11 | }, 12 | { 13 | href: "https://twitter.com/appwrite_io", 14 | icon: , 15 | }, 16 | { 17 | href: "http://appwrite.io", 18 | icon: , 19 | }, 20 | ]; 21 | 22 | const Landing = () => { 23 | const [user, setUser] = useRecoilState(userState); 24 | const router = useRouter(); 25 | 26 | useEffect(() => { 27 | appwrite.account.get().then((response: User) => { 28 | setUser(response); 29 | router.replace("/todos"); 30 | }, () => { 31 | console.log('no session found') 32 | }) 33 | }, []); 34 | 35 | return ( 36 | <> 37 | {/* Hero */} 38 |
    39 |
    40 |

    Introducing

    41 |

    toTooooDoooo

    42 |

    43 | A Simple To-do App built with Appwrite and 44 | Next.js 45 |

    46 | 50 | Get Started 51 | 52 |
    53 |
    54 | 55 |
    56 | {links.map((item, index) => ( 57 |
    58 | {item["icon"]} 59 |
    60 | ))} 61 |
    62 | 63 | ) 64 | }; 65 | 66 | export default Landing; 67 | -------------------------------------------------------------------------------- /pages/login.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { useState } from "react"; 3 | import type { FormEvent } from "react" 4 | import Alert from "../components/alert"; 5 | import { useRouter } from "next/router"; 6 | import { appwrite, userState } from "../store/global"; 7 | import { useRecoilState } from "recoil"; 8 | import { User } from '../store/types'; 9 | 10 | const Login = () => { 11 | const [email, setEmail] = useState(""); 12 | const [password, setPassword] = useState(""); 13 | const [alert, setAlert] = useState(""); 14 | const [user, setUser] = useRecoilState(userState); 15 | const router = useRouter(); 16 | 17 | const login = async (e: FormEvent) => { 18 | e.preventDefault(); 19 | try { 20 | setUser(await appwrite.account.createEmailSession(email, password) as unknown as User); 21 | router.push("/todos"); 22 | } catch (error) { 23 | setAlert(error.message); 24 | } 25 | } 26 | 27 | return ( 28 | <> 29 | {alert && } 30 |
    31 |
    32 |

    Login

    33 |

    34 | Don't have an account?{" "} 35 | 36 | 37 | Sign Up 38 | 39 | 40 |

    41 |
    42 | 43 | setEmail(e.target.value)} 49 | /> 50 | 51 | setPassword(e.target.value)} 57 | /> 58 | 59 |
    60 | 67 |
    68 |
    69 |
    70 |
    71 | 72 | ) 73 | } 74 | 75 | export default Login; 76 | -------------------------------------------------------------------------------- /pages/signup.tsx: -------------------------------------------------------------------------------- 1 | import { FormEvent, useState } from "react"; 2 | import Link from 'next/link' 3 | import { useRouter } from "next/router"; 4 | import Alert from "../components/alert"; 5 | import { appwrite, userState } from "../store/global"; 6 | import { useRecoilState } from 'recoil'; 7 | import { User } from '../store/types'; 8 | 9 | const SignUp = () => { 10 | const [name, setName] = useState(""); 11 | const [email, setEmail] = useState(""); 12 | const [password, setPassword] = useState(""); 13 | const [alert, setAlert] = useState(""); 14 | const [user, setUser] = useRecoilState(userState); 15 | const router = useRouter(); 16 | 17 | const signup = async (e: FormEvent) => { 18 | e.preventDefault(); 19 | try { 20 | await appwrite.account.create('unique()', email, password, name); 21 | setUser(await appwrite.account.createEmailSession(email, password) as unknown as User); 22 | router.push("/todos"); 23 | } catch (error) { 24 | setAlert(error.message); 25 | } 26 | } 27 | 28 | return ( 29 | <> 30 | {alert && } 31 |
    32 |
    33 |

    Sign Up

    34 |

    35 | Already have an account?{" "} 36 | 37 | 38 | Login 39 | 40 | 41 |

    42 |
    43 | 44 | setName(e.target.value)} 48 | /> 49 | 50 | 51 | {/* “Don’t worry if it doesn’t work right. If everything did, you’d be out of a job.” */} 52 | setEmail(e.target.value)} 56 | /> 57 | 58 | setPassword(e.target.value)} 62 | /> 63 | 64 |
    65 | 71 |
    72 |
    73 |
    74 |
    75 | 76 | ); 77 | }; 78 | 79 | export default SignUp; 80 | -------------------------------------------------------------------------------- /pages/todos.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { FormEvent, useEffect, useState } from "react"; 3 | import { useRecoilState } from "recoil"; 4 | import TodoItem from "../components/todoItem"; 5 | import { appwrite, todoState, userState } from "../store/global"; 6 | import { User } from "../store/types"; 7 | import { getJWT, setJWT } from "../utils/jwt"; 8 | 9 | 10 | const Todo = () => { 11 | const [currentTodo, setCurrentTodo] = useState(""); 12 | const [isLoading, setIsLoading] = useState(true); 13 | const [todos, setTodos] = useRecoilState(todoState); 14 | const [user, setUser] = useRecoilState(userState); 15 | 16 | const router = useRouter(); 17 | const addTodo = async (e: FormEvent) => { 18 | e.preventDefault(); 19 | const jwt = await getJWT(); 20 | const res = await fetch('/api/todos', { 21 | method: 'post', 22 | headers: { 23 | JWT: jwt 24 | }, 25 | body: JSON.stringify({ 26 | user: user.$id, 27 | todo: { 28 | content: currentTodo, 29 | isComplete: false 30 | } 31 | }) 32 | }); 33 | const json = await res.json(); 34 | setTodos(todos.concat(json)); 35 | setCurrentTodo(""); 36 | } 37 | 38 | 39 | const logout = async () => { 40 | await appwrite.account.deleteSession('current'); 41 | window.localStorage.removeItem("jwt"); 42 | window.localStorage.removeItem("jwt_expire"); 43 | router.push("/"); 44 | } 45 | 46 | useEffect(() => { 47 | const fetchData = async () => { 48 | const jwt = await getJWT(); 49 | const res = await fetch('/api/todos', { 50 | headers: { 51 | JWT: jwt 52 | } 53 | }); 54 | const json = await res.json() 55 | setTodos(json.documents); 56 | setIsLoading(false); 57 | } 58 | fetchData() 59 | }, [user]) 60 | 61 | useEffect(() => { 62 | if (user) return; 63 | const fetchData = async () => { 64 | const response = await appwrite.account.get(); 65 | setUser(response as User); 66 | } 67 | fetchData() 68 | }, []) 69 | 70 | return ( 71 | <> 72 |
    73 |
    74 |
    75 | 📝
      toTooooDoooos 76 |
    77 |
    78 | setCurrentTodo(e.target.value)} 85 | > 86 |
    87 | 88 | {isLoading &&

    Loading ....

    } 89 | 90 |
      91 | {todos.map((item) => ( 92 | 93 | ))} 94 |
    95 |
    96 |
    97 | 98 |
    99 | 102 |
    103 | 104 | ); 105 | }; 106 | 107 | export default Todo; 108 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/assets/appwrite.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/assets/delete.svg: -------------------------------------------------------------------------------- 1 | 7 | 13 | -------------------------------------------------------------------------------- /public/assets/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /public/assets/message (1).txt: -------------------------------------------------------------------------------- 1 | { 2 | "message": "Invalid document structure: Property \"tokens\" has invalid input. Invalid document structure: Unknown properties are not allowed (countryCode, osCode, osName, osVersion, clientType, clientCode, clientName, clientVersion, clientEngine, clientEngineVersion, deviceName, deviceBrand, deviceModel) for this collection. Make sure to follow token collection structure", 3 | "code": 0, 4 | "version": "dev", 5 | "file": "/usr/src/code/src/Appwrite/Database/Database.php", 6 | "line": 289, 7 | "trace": [ 8 | { 9 | "file": "/usr/src/code/app/controllers/api/account.php", 10 | "line": 216, 11 | "function": "updateDocument", 12 | "class": "Appwrite\\Database\\Database", 13 | "type": "->", 14 | "args": [ 15 | { 16 | "$id": "60691b6d8d45f", 17 | "$collection": "users", 18 | "$permissions": { 19 | "read": [ 20 | "*" 21 | ], 22 | "write": [ 23 | "user:{self}" 24 | ] 25 | }, 26 | "email": "test@testme.com", 27 | "emailVerification": false, 28 | "status": 0, 29 | "password": "$2y$08$cgkT0xgWLqWFkNjTgtWPzuxK2F7ZADugbRjj6vspsY7e3u.jq8xn6", 30 | "passwordUpdate": 1617501037, 31 | "registration": 1617501037, 32 | "reset": false, 33 | "name": "test", 34 | "memberships": [ 35 | { 36 | "$id": "60691baec48da", 37 | "$collection": "memberships", 38 | "$permissions": { 39 | "read": [ 40 | "user:60691b6d8d45f", 41 | "team:60691baebdd94" 42 | ], 43 | "write": [ 44 | "user:60691b6d8d45f", 45 | "team:60691baebdd94/owner" 46 | ] 47 | }, 48 | "userId": "60691b6d8d45f", 49 | "teamId": "60691baebdd94", 50 | "roles": [ 51 | "owner" 52 | ], 53 | "invited": 1617501102, 54 | "joined": 1617501102, 55 | "confirm": true, 56 | "secret": "" 57 | }, 58 | { 59 | "$id": "606961e262436", 60 | "$collection": "memberships", 61 | "$permissions": { 62 | "read": [ 63 | "user:60691b6d8d45f", 64 | "team:606961e25bbf0" 65 | ], 66 | "write": [ 67 | "user:60691b6d8d45f", 68 | "team:606961e25bbf0/owner" 69 | ] 70 | }, 71 | "userId": "60691b6d8d45f", 72 | "teamId": "606961e25bbf0", 73 | "roles": [ 74 | "owner" 75 | ], 76 | "invited": 1617519074, 77 | "joined": 1617519074, 78 | "confirm": true, 79 | "secret": "" 80 | }, 81 | { 82 | "$id": "606eba77842f7", 83 | "$collection": "memberships", 84 | "$permissions": { 85 | "read": [ 86 | "user:60691b6d8d45f", 87 | "team:606eba77729b1" 88 | ], 89 | "write": [ 90 | "user:60691b6d8d45f", 91 | "team:606eba77729b1/owner" 92 | ] 93 | }, 94 | "userId": "60691b6d8d45f", 95 | "teamId": "606eba77729b1", 96 | "roles": [ 97 | "owner" 98 | ], 99 | "invited": 1617869431, 100 | "joined": 1617869431, 101 | "confirm": true, 102 | "secret": "" 103 | } 104 | ], 105 | "tokens": [ 106 | { 107 | "$id": "60691bf5d4f03", 108 | "$collection": "tokens", 109 | "$permissions": { 110 | "read": [ 111 | "user:60691b6d8d45f" 112 | ], 113 | "write": [ 114 | "user:60691b6d8d45f" 115 | ] 116 | }, 117 | "userId": "60691b6d8d45f", 118 | "type": 1, 119 | "secret": "e922fc41d16961bc25e53193954131aa080c9e3b22bf9f560d7509b5df85acd1", 120 | "expire": 1649037173, 121 | "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36", 122 | "ip": "172.18.0.1", 123 | "countryCode": "--", 124 | "osCode": "LIN", 125 | "osName": "GNU/Linux", 126 | "osVersion": "", 127 | "clientType": "browser", 128 | "clientCode": "CH", 129 | "clientName": "Chrome", 130 | "clientVersion": "89.0", 131 | "clientEngine": "Blink", 132 | "clientEngineVersion": "", 133 | "deviceName": "desktop", 134 | "deviceBrand": "", 135 | "deviceModel": "" 136 | }, 137 | { 138 | "$id": "606961cfc3c94", 139 | "$collection": "tokens", 140 | "$permissions": { 141 | "read": [ 142 | "user:60691b6d8d45f" 143 | ], 144 | "write": [ 145 | "user:60691b6d8d45f" 146 | ] 147 | }, 148 | "userId": "60691b6d8d45f", 149 | "type": 1, 150 | "secret": "df3c75fd0f1b0cea4c28d8eab1018959dd881abc723107b1ab2245ef0a98328d", 151 | "expire": 1649055055, 152 | "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36", 153 | "ip": "172.18.0.1", 154 | "countryCode": "--", 155 | "osCode": "LIN", 156 | "osName": "GNU/Linux", 157 | "osVersion": "", 158 | "clientType": "browser", 159 | "clientCode": "CH", 160 | "clientName": "Chrome", 161 | "clientVersion": "89.0", 162 | "clientEngine": "Blink", 163 | "clientEngineVersion": "", 164 | "deviceName": "desktop", 165 | "deviceBrand": "", 166 | "deviceModel": "" 167 | } 168 | ], 169 | "sessions": [ 170 | { 171 | "$id": "607838909c4b3", 172 | "$permissions": { 173 | "read": [ 174 | "user:60691b6d8d45f" 175 | ], 176 | "write": [ 177 | "user:60691b6d8d45f" 178 | ] 179 | }, 180 | "$collection": "sessions", 181 | "userId": "60691b6d8d45f", 182 | "provider": "email", 183 | "providerUid": "test@testme.com", 184 | "secret": "d30e060e85b999b22a892f36dc8f04a00404e795384a26e53a66a5de75f5aee9", 185 | "expire": 1650027536, 186 | "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.128 Safari/537.36", 187 | "ip": "172.18.0.1", 188 | "countryCode": "--", 189 | "osCode": "LIN", 190 | "osName": "GNU/Linux", 191 | "osVersion": "", 192 | "clientType": "browser", 193 | "clientCode": "CH", 194 | "clientName": "Chrome", 195 | "clientVersion": "89.0", 196 | "clientEngine": "Blink", 197 | "clientEngineVersion": "", 198 | "deviceName": "desktop", 199 | "deviceBrand": "", 200 | "deviceModel": "" 201 | } 202 | ] 203 | } 204 | ] 205 | }, 206 | { 207 | "file": "/usr/src/code/vendor/utopia-php/framework/src/App.php", 208 | "line": 567, 209 | "function": "{closure}", 210 | "args": [ 211 | "test@testme.com", 212 | "testtest", 213 | {}, 214 | {}, 215 | {}, 216 | { 217 | "default": "en" 218 | }, 219 | {}, 220 | {} 221 | ] 222 | }, 223 | { 224 | "file": "/usr/src/code/vendor/utopia-php/framework/src/App.php", 225 | "line": 661, 226 | "function": "execute", 227 | "class": "Utopia\\App", 228 | "type": "->", 229 | "args": [ 230 | {}, 231 | { 232 | "email": "test@testme.com", 233 | "password": "testtest" 234 | } 235 | ] 236 | }, 237 | { 238 | "file": "/usr/src/code/app/http.php", 239 | "line": 103, 240 | "function": "run", 241 | "class": "Utopia\\App", 242 | "type": "->", 243 | "args": [ 244 | {}, 245 | {} 246 | ] 247 | } 248 | ] 249 | } -------------------------------------------------------------------------------- /public/assets/message.txt: -------------------------------------------------------------------------------- 1 | { 2 | "message": "Invalid document structure: Property \"tokens\" has invalid input. Invalid document structure: Unknown properties are not allowed (countryCode, osCode, osName, osVersion, clientType, clientCode, clientName, clientVersion, clientEngine, clientEngineVersion, deviceName, deviceBrand, deviceModel) for this collection. Make sure to follow token collection structure", 3 | "code": 0, 4 | "version": "dev", 5 | "file": "/usr/src/code/src/Appwrite/Database/Database.php", 6 | "line": 289, 7 | "trace": [ 8 | { 9 | "file": "/usr/src/code/app/controllers/api/account.php", 10 | "line": 216, 11 | "function": "updateDocument", 12 | "class": "Appwrite\\Database\\Database", 13 | "type": "->", 14 | "args": [ 15 | { 16 | "$id": "60691b6d8d45f", 17 | "$collection": "users", 18 | "$permissions": { 19 | "read": [ 20 | "*" 21 | ], 22 | "write": [ 23 | "user:{self}" 24 | ] 25 | }, 26 | "email": "test@testme.com", 27 | "emailVerification": false, 28 | "status": 0, 29 | "password": "$2y$08$cgkT0xgWLqWFkNjTgtWPzuxK2F7ZADugbRjj6vspsY7e3u.jq8xn6", 30 | "passwordUpdate": 1617501037, 31 | "registration": 1617501037, 32 | "reset": false, 33 | "name": "test", 34 | "memberships": [ 35 | { 36 | "$id": "60691baec48da", 37 | "$collection": "memberships", 38 | "$permissions": { 39 | "read": [ 40 | "user:60691b6d8d45f", 41 | "team:60691baebdd94" 42 | ], 43 | "write": [ 44 | "user:60691b6d8d45f", 45 | "team:60691baebdd94/owner" 46 | ] 47 | }, 48 | "userId": "60691b6d8d45f", 49 | "teamId": "60691baebdd94", 50 | "roles": [ 51 | "owner" 52 | ], 53 | "invited": 1617501102, 54 | "joined": 1617501102, 55 | "confirm": true, 56 | "secret": "" 57 | }, 58 | { 59 | "$id": "606961e262436", 60 | "$collection": "memberships", 61 | "$permissions": { 62 | "read": [ 63 | "user:60691b6d8d45f", 64 | "team:606961e25bbf0" 65 | ], 66 | "write": [ 67 | "user:60691b6d8d45f", 68 | "team:606961e25bbf0/owner" 69 | ] 70 | }, 71 | "userId": "60691b6d8d45f", 72 | "teamId": "606961e25bbf0", 73 | "roles": [ 74 | "owner" 75 | ], 76 | "invited": 1617519074, 77 | "joined": 1617519074, 78 | "confirm": true, 79 | "secret": "" 80 | }, 81 | { 82 | "$id": "606eba77842f7", 83 | "$collection": "memberships", 84 | "$permissions": { 85 | "read": [ 86 | "user:60691b6d8d45f", 87 | "team:606eba77729b1" 88 | ], 89 | "write": [ 90 | "user:60691b6d8d45f", 91 | "team:606eba77729b1/owner" 92 | ] 93 | }, 94 | "userId": "60691b6d8d45f", 95 | "teamId": "606eba77729b1", 96 | "roles": [ 97 | "owner" 98 | ], 99 | "invited": 1617869431, 100 | "joined": 1617869431, 101 | "confirm": true, 102 | "secret": "" 103 | } 104 | ], 105 | "tokens": [ 106 | { 107 | "$id": "60691bf5d4f03", 108 | "$collection": "tokens", 109 | "$permissions": { 110 | "read": [ 111 | "user:60691b6d8d45f" 112 | ], 113 | "write": [ 114 | "user:60691b6d8d45f" 115 | ] 116 | }, 117 | "userId": "60691b6d8d45f", 118 | "type": 1, 119 | "secret": "e922fc41d16961bc25e53193954131aa080c9e3b22bf9f560d7509b5df85acd1", 120 | "expire": 1649037173, 121 | "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36", 122 | "ip": "172.18.0.1", 123 | "countryCode": "--", 124 | "osCode": "LIN", 125 | "osName": "GNU/Linux", 126 | "osVersion": "", 127 | "clientType": "browser", 128 | "clientCode": "CH", 129 | "clientName": "Chrome", 130 | "clientVersion": "89.0", 131 | "clientEngine": "Blink", 132 | "clientEngineVersion": "", 133 | "deviceName": "desktop", 134 | "deviceBrand": "", 135 | "deviceModel": "" 136 | }, 137 | { 138 | "$id": "606961cfc3c94", 139 | "$collection": "tokens", 140 | "$permissions": { 141 | "read": [ 142 | "user:60691b6d8d45f" 143 | ], 144 | "write": [ 145 | "user:60691b6d8d45f" 146 | ] 147 | }, 148 | "userId": "60691b6d8d45f", 149 | "type": 1, 150 | "secret": "df3c75fd0f1b0cea4c28d8eab1018959dd881abc723107b1ab2245ef0a98328d", 151 | "expire": 1649055055, 152 | "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36", 153 | "ip": "172.18.0.1", 154 | "countryCode": "--", 155 | "osCode": "LIN", 156 | "osName": "GNU/Linux", 157 | "osVersion": "", 158 | "clientType": "browser", 159 | "clientCode": "CH", 160 | "clientName": "Chrome", 161 | "clientVersion": "89.0", 162 | "clientEngine": "Blink", 163 | "clientEngineVersion": "", 164 | "deviceName": "desktop", 165 | "deviceBrand": "", 166 | "deviceModel": "" 167 | } 168 | ], 169 | "sessions": [ 170 | { 171 | "$id": "607838909c4b3", 172 | "$permissions": { 173 | "read": [ 174 | "user:60691b6d8d45f" 175 | ], 176 | "write": [ 177 | "user:60691b6d8d45f" 178 | ] 179 | }, 180 | "$collection": "sessions", 181 | "userId": "60691b6d8d45f", 182 | "provider": "email", 183 | "providerUid": "test@testme.com", 184 | "secret": "d30e060e85b999b22a892f36dc8f04a00404e795384a26e53a66a5de75f5aee9", 185 | "expire": 1650027536, 186 | "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.128 Safari/537.36", 187 | "ip": "172.18.0.1", 188 | "countryCode": "--", 189 | "osCode": "LIN", 190 | "osName": "GNU/Linux", 191 | "osVersion": "", 192 | "clientType": "browser", 193 | "clientCode": "CH", 194 | "clientName": "Chrome", 195 | "clientVersion": "89.0", 196 | "clientEngine": "Blink", 197 | "clientEngineVersion": "", 198 | "deviceName": "desktop", 199 | "deviceBrand": "", 200 | "deviceModel": "" 201 | } 202 | ] 203 | } 204 | ] 205 | }, 206 | { 207 | "file": "/usr/src/code/vendor/utopia-php/framework/src/App.php", 208 | "line": 567, 209 | "function": "{closure}", 210 | "args": [ 211 | "test@testme.com", 212 | "testtest", 213 | {}, 214 | {}, 215 | {}, 216 | { 217 | "default": "en" 218 | }, 219 | {}, 220 | {} 221 | ] 222 | }, 223 | { 224 | "file": "/usr/src/code/vendor/utopia-php/framework/src/App.php", 225 | "line": 661, 226 | "function": "execute", 227 | "class": "Utopia\\App", 228 | "type": "->", 229 | "args": [ 230 | {}, 231 | { 232 | "email": "test@testme.com", 233 | "password": "testtest" 234 | } 235 | ] 236 | }, 237 | { 238 | "file": "/usr/src/code/app/http.php", 239 | "line": 103, 240 | "function": "run", 241 | "class": "Utopia\\App", 242 | "type": "->", 243 | "args": [ 244 | {}, 245 | {} 246 | ] 247 | } 248 | ] 249 | } -------------------------------------------------------------------------------- /public/assets/nextjs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | next-black 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/assets/svelte.svg: -------------------------------------------------------------------------------- 1 | svelte-logo -------------------------------------------------------------------------------- /public/assets/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appwrite/demo-todo-with-nextjs/596e762089ee0fc358e41c16654d09b47de716f2/public/favicon.ico -------------------------------------------------------------------------------- /store/global.tsx: -------------------------------------------------------------------------------- 1 | import { Client as Appwrite, Account, Databases } from "appwrite"; 2 | import { atom } from "recoil"; 3 | import { Todo, User } from "./types"; 4 | // “In a software project team of ten, there are probably three people who produce enough defects to make them net-negative producers.” 5 | export const Server = { 6 | endpoint: process.env.REACT_APP_ENDPOINT, 7 | project: process.env.REACT_APP_PROJECT, 8 | collectionID: process.env.REACT_APP_COLLECTION_ID, 9 | databaseID: process.env.REACT_APP_DATABASE_ID, 10 | }; 11 | 12 | export const client = new Appwrite() 13 | .setEndpoint(Server.endpoint) 14 | .setProject(Server.project); 15 | const account = new Account(client); 16 | const database = new Databases(client, Server.databaseID); 17 | 18 | export const appwrite = { account, database }; 19 | 20 | export const todoState = atom({ 21 | key: "todos", 22 | default: [], 23 | }); 24 | 25 | export const userState = atom({ 26 | key: "user", 27 | default: null, 28 | }); 29 | -------------------------------------------------------------------------------- /store/types.tsx: -------------------------------------------------------------------------------- 1 | import type { Client as Appwrite } from "appwrite"; 2 | 3 | type Todo = { 4 | $id?: string; 5 | isComplete: boolean; 6 | content: string; 7 | }; 8 | 9 | type Alert = { 10 | color: "red"; 11 | message: string; 12 | }; 13 | 14 | type User = { 15 | $id: string; 16 | email: string; 17 | name: string; 18 | }; 19 | 20 | type State = { 21 | todos: Todo[]; 22 | user?: User; 23 | appwrite?: Appwrite; 24 | }; 25 | 26 | export type { 27 | Todo, 28 | Alert, 29 | User, 30 | State 31 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'], 3 | darkMode: false, // or 'media' or 'class' 4 | theme: { 5 | extend: {}, 6 | }, 7 | variants: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": false, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve" 20 | }, 21 | "include": [ 22 | "next-env.d.ts", 23 | "**/*.ts", 24 | "**/*.tsx" 25 | ], 26 | "exclude": [ 27 | "node_modules" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /utils/jwt.ts: -------------------------------------------------------------------------------- 1 | import { appwrite } from "../store/global"; 2 | 3 | export const getJWT = async () => { 4 | const jwt = window?.localStorage.getItem("jwt"); 5 | const jwtExpires = +window?.localStorage.getItem("jwt_expires"); 6 | if (jwt && jwtExpires > Date.now()) { 7 | return jwt; 8 | } else { 9 | try { 10 | setJWT((await appwrite.account.createJWT()).jwt); 11 | return await getJWT(); 12 | } catch (error) { 13 | return null; 14 | } 15 | } 16 | } 17 | export const setJWT = (jwt: string) => { 18 | const jwtExpires = Date.now() + (15*60*1000); 19 | window?.localStorage.setItem("jwt", jwt); 20 | window?.localStorage.setItem("jwt_expires", jwtExpires.toString()); 21 | } --------------------------------------------------------------------------------