├── .DS_Store ├── Part-1 ├── .eslintrc.cjs ├── .gitignore ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public │ └── vite.svg ├── src │ ├── App.css │ ├── App.jsx │ ├── appwrite │ │ ├── config.js │ │ └── databases.js │ ├── assets │ │ ├── DeleteIcon.jsx │ │ └── react.svg │ ├── components │ │ ├── Note.jsx │ │ └── NoteForm.jsx │ ├── index.css │ ├── main.jsx │ └── pages │ │ ├── LoginRegister.jsx │ │ └── Notes.jsx └── vite.config.js ├── Part-2-Template-Only ├── .eslintrc.cjs ├── .gitignore ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public │ └── vite.svg ├── src │ ├── App.jsx │ ├── assets │ │ ├── DeleteIcon.jsx │ │ └── react.svg │ ├── components │ │ ├── Note.jsx │ │ └── NoteForm.jsx │ ├── data.json │ ├── index.css │ ├── main.jsx │ └── pages │ │ ├── LoginRegister.jsx │ │ └── Notes.jsx └── vite.config.js ├── Part-2 ├── .eslintrc.cjs ├── .gitignore ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public │ └── vite.svg ├── src │ ├── App.css │ ├── App.jsx │ ├── appwrite │ │ ├── config.js │ │ └── databases.js │ ├── assets │ │ ├── DeleteIcon.jsx │ │ └── react.svg │ ├── components │ │ ├── Note.jsx │ │ ├── NoteForm.jsx │ │ └── ThemeOption.jsx │ ├── index.css │ ├── main.jsx │ └── pages │ │ ├── LoginRegister.jsx │ │ └── Notes.jsx └── vite.config.js ├── Part-3 ├── .env.example ├── .eslintrc.cjs ├── .gitignore ├── index.html ├── package-lock.json ├── package.json ├── public │ └── vite.svg ├── src │ ├── App.jsx │ ├── appwrite │ │ ├── config.js │ │ └── databases.js │ ├── assets │ │ ├── DeleteIcon.jsx │ │ ├── LockIcon.jsx │ │ ├── login.png │ │ └── react.svg │ ├── components │ │ ├── LoginForm.jsx │ │ ├── LogoutButton.jsx │ │ ├── Note.jsx │ │ ├── NoteForm.jsx │ │ ├── PrivateRoutes.jsx │ │ ├── RegisterFrom.jsx │ │ └── ThemeOption.jsx │ ├── context │ │ └── AuthContext.jsx │ ├── index.css │ ├── main.jsx │ └── pages │ │ ├── LoginRegister.jsx │ │ └── Notes.jsx └── vite.config.js ├── README.md ├── assets ├── .env.example └── notes.png └── package-lock.json /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/divanov11/Appwrite-React-quickstart/658881d728939d622e0f5ac9c4b8eb093a2cbcb2/.DS_Store -------------------------------------------------------------------------------- /Part-1/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react-refresh/only-export-components': [ 16 | 'warn', 17 | { allowConstantExport: true }, 18 | ], 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /Part-1/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | .env 10 | 11 | node_modules 12 | dist 13 | dist-ssr 14 | *.local 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /Part-1/README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | -------------------------------------------------------------------------------- /Part-1/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Part-1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "appwrite": "^14.0.1", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0", 16 | "react-router-dom": "^6.23.1" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^18.2.15", 20 | "@types/react-dom": "^18.2.7", 21 | "@vitejs/plugin-react": "^4.0.3", 22 | "eslint": "^8.45.0", 23 | "eslint-plugin-react": "^7.32.2", 24 | "eslint-plugin-react-hooks": "^4.6.0", 25 | "eslint-plugin-react-refresh": "^0.4.3", 26 | "vite": "^4.4.5" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Part-1/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Part-1/src/App.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/divanov11/Appwrite-React-quickstart/658881d728939d622e0f5ac9c4b8eb093a2cbcb2/Part-1/src/App.css -------------------------------------------------------------------------------- /Part-1/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter, Routes, Route } from "react-router-dom"; 2 | import Notes from "./pages/Notes"; 3 | import LoginRegister from "./pages/LoginRegister"; 4 | 5 | function App() { 6 | return ( 7 |
8 |
9 | 10 | 11 | } path="/" /> 12 | } path="/login" /> 13 | 14 | 15 |
16 |
17 | ); 18 | } 19 | 20 | export default App; 21 | -------------------------------------------------------------------------------- /Part-1/src/appwrite/config.js: -------------------------------------------------------------------------------- 1 | import { Client, Databases } from "appwrite"; 2 | 3 | const client = new Client(); 4 | 5 | client 6 | .setEndpoint(import.meta.env.VITE_ENDPOINT) 7 | .setProject(import.meta.env.VITE_PROJECT_ID); 8 | 9 | const databases = new Databases(client); 10 | 11 | export { client, databases }; 12 | -------------------------------------------------------------------------------- /Part-1/src/appwrite/databases.js: -------------------------------------------------------------------------------- 1 | import { databases } from "./config"; 2 | import { ID } from "appwrite"; 3 | 4 | const db = {}; 5 | 6 | const collections = [ 7 | { 8 | dbId: import.meta.env.VITE_DATABASE_ID, 9 | id: import.meta.env.VITE_COLLECTION_ID_NOTES, 10 | name: "notes", 11 | }, 12 | ]; 13 | 14 | collections.forEach((col) => { 15 | db[col.name] = { 16 | create: (payload, permissions, id = ID.unique()) => 17 | databases.createDocument( 18 | col.dbId, 19 | col.id, 20 | id, 21 | payload, 22 | permissions 23 | ), 24 | update: (id, payload, permissions) => 25 | databases.updateDocument( 26 | col.dbId, 27 | col.id, 28 | id, 29 | payload, 30 | permissions 31 | ), 32 | delete: (id) => databases.deleteDocument(col.dbId, col.id, id), 33 | 34 | list: (queries = []) => 35 | databases.listDocuments(col.dbId, col.id, queries), 36 | 37 | get: (id) => databases.getDocument(col.dbId, col.id, id), 38 | }; 39 | }); 40 | 41 | export default db; 42 | -------------------------------------------------------------------------------- /Part-1/src/assets/DeleteIcon.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const DeleteIcon = ({ size = 24, stroke = "#f87171" }) => { 4 | return ( 5 | 15 | 16 | 20 | 21 | ); 22 | }; 23 | 24 | export default DeleteIcon; 25 | -------------------------------------------------------------------------------- /Part-1/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Part-1/src/components/Note.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import db from "../appwrite/databases"; 3 | import DeleteIcon from "../assets/DeleteIcon"; 4 | 5 | function Note({ setNotes, noteData }) { 6 | const [note, setNote] = useState(noteData); 7 | 8 | const handleUpdate = async () => { 9 | const completed = !note.completed; 10 | db.notes.update(note.$id, { completed }); 11 | setNote({ ...note, completed: completed }); 12 | }; 13 | 14 | const handleDelete = async () => { 15 | db.notes.delete(note.$id); 16 | setNotes((prevState) => prevState.filter((i) => i.$id !== note.$id)); 17 | }; 18 | 19 | return ( 20 |
21 | 22 | {note.completed ? {note.body} : <>{note.body}} 23 | 24 | 25 |
26 | 27 |
28 |
29 | ); 30 | } 31 | 32 | export default Note; 33 | -------------------------------------------------------------------------------- /Part-1/src/components/NoteForm.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import db from "../appwrite/databases"; 3 | 4 | function NoteForm({ setNotes }) { 5 | const handleAdd = async (e) => { 6 | e.preventDefault(); 7 | const noteBody = e.target.body.value; 8 | 9 | if (noteBody === "") return; 10 | 11 | try { 12 | const payload = { body: noteBody }; 13 | 14 | const response = await db.notes.create(payload); 15 | setNotes((prevState) => [response, ...prevState]); 16 | 17 | e.target.reset(); 18 | } catch (error) { 19 | console.error(error); 20 | } 21 | }; 22 | 23 | return ( 24 |
25 | 30 |
31 | ); 32 | } 33 | 34 | export default NoteForm; 35 | -------------------------------------------------------------------------------- /Part-1/src/index.css: -------------------------------------------------------------------------------- 1 | :root{ 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-size: 18px; 5 | font-weight: 400; 6 | 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | -webkit-text-size-adjust: 100%; 10 | 11 | 12 | 13 | --primary-background-color: #fff; 14 | --secondary-background-color:#f7f7f7; 15 | 16 | --primary-text-color:#000; 17 | --secondary-text-color:#4b4c53; 18 | 19 | --primary-border-color:#4b4c53; 20 | 21 | --button-solid-background-color:#000; 22 | --button-solid-text-color:#fff; 23 | 24 | --button-outlined-background-color:transparent; 25 | --button-outlined-text-color:#000; 26 | 27 | --alert-color:#FFF9E7; 28 | --alert-border-color:#FFBF00; 29 | --alert-text-color:#b8952f; 30 | } 31 | 32 | [data-theme="dark"]{ 33 | --primary-background-color: #1f2028; 34 | --secondary-background-color:#2e3039; 35 | 36 | --primary-text-color:rgba(255, 255, 255, 0.87); 37 | --secondary-text-color:#4b4c53; 38 | 39 | --primary-border-color:#4b4c53; 40 | 41 | --button-solid-background-color:#fff; 42 | --button-solid-text-color:#000; 43 | 44 | --button-outlined-background-color:transparent; 45 | --button-outlined-text-color:#fff; 46 | 47 | } 48 | 49 | [data-theme="purple"]{ 50 | --primary-background-color: rgba(24, 16, 40, 1); 51 | --secondary-background-color:rgba(44, 36, 70, 1); 52 | 53 | --primary-text-color:rgba(255, 255, 255, 0.87); 54 | --secondary-text-color:#8549a7; 55 | 56 | --primary-border-color:#8549a7; 57 | 58 | --button-solid-background-color:#fff; 59 | --button-solid-text-color:#000; 60 | 61 | --button-outlined-background-color:transparent; 62 | --button-outlined-text-color:#fff; 63 | 64 | } 65 | 66 | 67 | html, body, #root{ 68 | padding: 0; 69 | margin: 0; 70 | height: 100%; 71 | width: 100%; 72 | } 73 | 74 | div{ 75 | overflow-y: auto; 76 | } 77 | 78 | #app{ 79 | height: 100%; 80 | background-color: var(--primary-background-color); 81 | color:var(--primary-text-color); 82 | transition: background-color 0.5s ease, color 0.5s ease; 83 | 84 | } 85 | 86 | #container{ 87 | max-width: 600px; 88 | margin: 0em auto; 89 | padding: 2em 0; 90 | } 91 | 92 | #todo-form{ 93 | margin: 2em 0; 94 | } 95 | 96 | #todo-form input{ 97 | border: 1px solid var(--primary-border-color); 98 | padding: 1em; 99 | border-radius: 10px; 100 | width: 100%; 101 | font-size: 18px; 102 | box-sizing: border-box; 103 | background-color: transparent; 104 | color: var(--primary-text-color); 105 | } 106 | 107 | .note-wrapper{ 108 | /* border-bottom:1px solid hsla(0, 1%,22%,1); */ 109 | background-color: var(--secondary-background-color); 110 | padding: 1em; 111 | margin: 1em 0; 112 | border-radius: 5px; 113 | display: flex; 114 | justify-content: space-between; 115 | /* align-items: center; */ 116 | } 117 | 118 | .note-body{ 119 | cursor: pointer; 120 | } 121 | 122 | .delete{ 123 | cursor: pointer; 124 | } 125 | 126 | .auth-field-wrapper{ 127 | 128 | display: flex; 129 | flex-direction: column; 130 | margin: 1em 0 ; 131 | 132 | 133 | } 134 | 135 | .auth-field-wrapper input[type='text'], input[type='email'], input[type='password']{ 136 | border: none; 137 | outline: none; 138 | background-color: transparent; 139 | height: 75px; 140 | box-sizing: border-box; 141 | border-bottom: 1px solid var(--primary-border-color); 142 | font-size: 18px; 143 | color: var(--button-outlined-text-color); 144 | } 145 | 146 | .auth-field-wrapper input[type='submit']{ 147 | padding: 1em 1.5em; 148 | 149 | width: fit-content; 150 | font-size: 18px; 151 | font-weight: 500; 152 | border:0; 153 | border-radius: 50px; 154 | background-color: var(--button-solid-background-color); 155 | color: var(--button-solid-text-color); 156 | } 157 | 158 | button{ 159 | padding: 1em 1.5em; 160 | 161 | width: fit-content; 162 | font-size: 18px; 163 | font-weight: 500; 164 | border:0; 165 | border-radius: 50px; 166 | border: 1.5px solid var(--button-outlined-text-color); 167 | background-color: transparent; 168 | color: var(--button-outlined-text-color);; 169 | } 170 | 171 | input[type="submit"], button{ 172 | cursor: pointer; 173 | } 174 | 175 | #logout-btn{ 176 | position: fixed; 177 | bottom: 2em; 178 | right: 2em; 179 | } 180 | 181 | .tag{ 182 | background-color:#61b593 ; 183 | padding: 0.5em 1em; 184 | border-radius: 20px; 185 | width: fit-content; 186 | font-size: 12px; 187 | font-weight: 700; 188 | color: #fff; 189 | } 190 | 191 | #pro-header{ 192 | display: flex; 193 | justify-content: space-between; 194 | align-items: center; 195 | } 196 | 197 | #subscribe-prompt{ 198 | display: flex; 199 | align-items:center; 200 | gap: 0.5em; 201 | background-color: var(--alert-color); 202 | border: 1px solid var(--alert-border-color); 203 | padding: 0 1em; 204 | border-radius: 5px; 205 | color: var(--alert-text-color); 206 | cursor: pointer; 207 | transition: 0.3s; 208 | } 209 | 210 | #subscribe-prompt:hover{ 211 | color: #886f23; 212 | background-color: #f0e4c1; 213 | } 214 | 215 | .theme-options{ 216 | display: flex; 217 | gap:10px; 218 | } 219 | 220 | .theme-option{ 221 | height: 25px; 222 | width: 25px; 223 | border: 1px solid; 224 | border-radius: 50%; 225 | cursor: pointer; 226 | } 227 | 228 | 229 | 230 | @media screen and (max-width:600px){ 231 | #app{ 232 | padding: 0 1em; 233 | } 234 | } -------------------------------------------------------------------------------- /Part-1/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.jsx' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /Part-1/src/pages/LoginRegister.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function LoginRegister() { 4 | return
LoginRegister
; 5 | } 6 | 7 | export default LoginRegister; 8 | -------------------------------------------------------------------------------- /Part-1/src/pages/Notes.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import db from "../appwrite/databases"; 3 | import NoteForm from "../components/NoteForm"; 4 | import { Query } from "appwrite"; 5 | import Note from "../components/Note"; 6 | 7 | function Notes() { 8 | const [notes, setNotes] = useState([]); 9 | 10 | useEffect(() => { 11 | init(); 12 | }, []); 13 | 14 | const init = async () => { 15 | const response = await db.notes.list([Query.orderDesc("$createdAt")]); 16 | setNotes(response.documents); 17 | }; 18 | 19 | return ( 20 | <> 21 |
22 |

✍️ My Todo List

23 |
24 | 25 | 26 | 27 |
28 | {notes.map((note) => ( 29 | 30 | ))} 31 |
32 | 33 | ); 34 | } 35 | 36 | export default Notes; 37 | -------------------------------------------------------------------------------- /Part-1/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /Part-2-Template-Only/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react-refresh/only-export-components': [ 16 | 'warn', 17 | { allowConstantExport: true }, 18 | ], 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /Part-2-Template-Only/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /Part-2-Template-Only/README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | -------------------------------------------------------------------------------- /Part-2-Template-Only/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Part-2-Template-Only/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0", 15 | "react-router-dom": "^6.23.1" 16 | }, 17 | "devDependencies": { 18 | "@types/react": "^18.2.15", 19 | "@types/react-dom": "^18.2.7", 20 | "@vitejs/plugin-react": "^4.0.3", 21 | "eslint": "^8.45.0", 22 | "eslint-plugin-react": "^7.32.2", 23 | "eslint-plugin-react-hooks": "^4.6.0", 24 | "eslint-plugin-react-refresh": "^0.4.3", 25 | "vite": "^4.4.5" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Part-2-Template-Only/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Part-2-Template-Only/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter, Routes, Route } from "react-router-dom"; 2 | import Notes from "./pages/Notes"; 3 | import LoginRegister from "./pages/LoginRegister"; 4 | 5 | function App() { 6 | return ( 7 |
8 |
9 | 10 | 11 | } path="/" /> 12 | } path="/login" /> 13 | 14 | 15 |
16 |
17 | ); 18 | } 19 | 20 | export default App; 21 | -------------------------------------------------------------------------------- /Part-2-Template-Only/src/assets/DeleteIcon.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const DeleteIcon = ({ size = 24, stroke = "#f87171" }) => { 4 | return ( 5 | 15 | 16 | 20 | 21 | ); 22 | }; 23 | 24 | export default DeleteIcon; 25 | -------------------------------------------------------------------------------- /Part-2-Template-Only/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Part-2-Template-Only/src/components/Note.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import DeleteIcon from "../assets/DeleteIcon"; 3 | 4 | function Note({ noteData }) { 5 | const [note, setNote] = useState(noteData); 6 | 7 | return ( 8 |
9 | 10 | {note.completed ? {note.body} : <>{note.body}} 11 | 12 | 13 |
14 | 15 |
16 |
17 | ); 18 | } 19 | 20 | export default Note; 21 | -------------------------------------------------------------------------------- /Part-2-Template-Only/src/components/NoteForm.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function NoteForm() { 4 | return ( 5 |
6 | 12 |
13 | ); 14 | } 15 | 16 | export default NoteForm; 17 | -------------------------------------------------------------------------------- /Part-2-Template-Only/src/data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$id":1, 4 | "body":"Wash car", 5 | "completed":false 6 | }, 7 | { 8 | "$id":2, 9 | "body":"Engineering sync with team", 10 | "completed":false 11 | }, 12 | { 13 | "$id":3, 14 | "body":"Clean house", 15 | "completed":true 16 | } 17 | ] -------------------------------------------------------------------------------- /Part-2-Template-Only/src/index.css: -------------------------------------------------------------------------------- 1 | :root{ 2 | 3 | --primary-background-color: #fff; 4 | --secondary-background-color:#f7f7f7; 5 | 6 | --primary-text-color:#000; 7 | --secondary-text-color:#4b4c53; 8 | 9 | --primary-border-color:#4b4c53; 10 | 11 | --button-solid-background-color:#000; 12 | --button-solid-text-color:#fff; 13 | 14 | --button-outlined-background-color:transparent; 15 | --button-outlined-text-color:#000; 16 | } 17 | 18 | html, body, #root{ 19 | padding: 0; 20 | margin: 0; 21 | height: 100%; 22 | width: 100%; 23 | 24 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 25 | line-height: 1.5; 26 | font-size: 18px; 27 | font-weight: 400; 28 | 29 | -webkit-font-smoothing: antialiased; 30 | -moz-osx-font-smoothing: grayscale; 31 | -webkit-text-size-adjust: 100%; 32 | } 33 | 34 | div{ 35 | overflow-y: auto; 36 | } 37 | 38 | #app{ 39 | height: 100%; 40 | background-color: var(--primary-background-color); 41 | color:var(--primary-text-color); 42 | 43 | } 44 | 45 | #container{ 46 | max-width: 600px; 47 | margin: 0em auto; 48 | padding: 2em 0; 49 | } 50 | 51 | #todo-form{ 52 | margin: 2em 0; 53 | } 54 | 55 | #todo-form input{ 56 | border: 1px solid var(--primary-border-color); 57 | padding: 1em; 58 | border-radius: 10px; 59 | width: 100%; 60 | font-size: 18px; 61 | box-sizing: border-box; 62 | background-color: transparent; 63 | color: var(--primary-text-color); 64 | } 65 | 66 | .note-wrapper{ 67 | /* border-bottom:1px solid hsla(0, 1%,22%,1); */ 68 | background-color: var(--secondary-background-color); 69 | padding: 1em; 70 | margin: 1em 0; 71 | border-radius: 5px; 72 | display: flex; 73 | justify-content: space-between; 74 | /* align-items: center; */ 75 | } 76 | 77 | .note-body{ 78 | cursor: pointer; 79 | } 80 | 81 | .delete{ 82 | cursor: pointer; 83 | } 84 | 85 | .auth-field-wrapper{ 86 | 87 | display: flex; 88 | flex-direction: column; 89 | margin: 1em 0 ; 90 | 91 | 92 | } 93 | 94 | .auth-field-wrapper input[type='text'], input[type='email'], input[type='password']{ 95 | border: none; 96 | outline: none; 97 | background-color: transparent; 98 | height: 75px; 99 | box-sizing: border-box; 100 | border-bottom: 1px solid var(--primary-border-color); 101 | font-size: 18px; 102 | color: var(--button-outlined-text-color); 103 | } 104 | 105 | .auth-field-wrapper input[type='submit']{ 106 | padding: 1em 1.5em; 107 | 108 | width: fit-content; 109 | font-size: 18px; 110 | font-weight: 500; 111 | border:0; 112 | border-radius: 50px; 113 | background-color: var(--button-solid-background-color); 114 | color: var(--button-solid-text-color); 115 | } 116 | 117 | button{ 118 | padding: 1em 1.5em; 119 | 120 | width: fit-content; 121 | font-size: 18px; 122 | font-weight: 500; 123 | border:0; 124 | border-radius: 50px; 125 | border: 1.5px solid var(--button-outlined-text-color); 126 | background-color: transparent; 127 | color: var(--button-outlined-text-color);; 128 | } 129 | 130 | input[type="submit"], button{ 131 | cursor: pointer; 132 | } 133 | 134 | #logout-btn{ 135 | position: fixed; 136 | bottom: 2em; 137 | right: 2em; 138 | } 139 | 140 | .tag{ 141 | background-color:#61b593 ; 142 | padding: 0.5em 1em; 143 | border-radius: 20px; 144 | width: fit-content; 145 | font-size: 12px; 146 | font-weight: 700; 147 | color: #fff; 148 | } 149 | 150 | #pro-header{ 151 | display: flex; 152 | justify-content: space-between; 153 | align-items: center; 154 | } 155 | 156 | #subscribe-prompt{ 157 | display: flex; 158 | align-items:center; 159 | gap: 0.5em; 160 | background-color: var(--alert-color); 161 | border: 1px solid var(--alert-border-color); 162 | padding: 0 1em; 163 | border-radius: 5px; 164 | color: var(--alert-text-color); 165 | cursor: pointer; 166 | transition: 0.3s; 167 | } 168 | 169 | #subscribe-prompt:hover{ 170 | color: #886f23; 171 | background-color: #f0e4c1; 172 | } 173 | 174 | @media screen and (max-width:600px){ 175 | #app{ 176 | padding: 0 1em; 177 | } 178 | } -------------------------------------------------------------------------------- /Part-2-Template-Only/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.jsx' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /Part-2-Template-Only/src/pages/LoginRegister.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function LoginRegister() { 4 | return
LoginRegister
; 5 | } 6 | 7 | export default LoginRegister; 8 | -------------------------------------------------------------------------------- /Part-2-Template-Only/src/pages/Notes.jsx: -------------------------------------------------------------------------------- 1 | import NoteForm from "../components/NoteForm"; 2 | import Note from "../components/Note"; 3 | import notes from "../data.json"; 4 | 5 | function Notes() { 6 | return ( 7 | <> 8 |
9 |

✍️ My Todo List

10 |
11 | 12 | 13 | 14 |
15 | {notes.map((note) => ( 16 | 17 | ))} 18 |
19 | 20 | ); 21 | } 22 | 23 | export default Notes; 24 | -------------------------------------------------------------------------------- /Part-2-Template-Only/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /Part-2/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react-refresh/only-export-components': [ 16 | 'warn', 17 | { allowConstantExport: true }, 18 | ], 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /Part-2/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | .env 10 | 11 | node_modules 12 | dist 13 | dist-ssr 14 | *.local 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /Part-2/README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | -------------------------------------------------------------------------------- /Part-2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Part-2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "appwrite": "^14.0.1", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0", 16 | "react-router-dom": "^6.23.1" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^18.2.15", 20 | "@types/react-dom": "^18.2.7", 21 | "@vitejs/plugin-react": "^4.0.3", 22 | "eslint": "^8.45.0", 23 | "eslint-plugin-react": "^7.32.2", 24 | "eslint-plugin-react-hooks": "^4.6.0", 25 | "eslint-plugin-react-refresh": "^0.4.3", 26 | "vite": "^4.4.5" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Part-2/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Part-2/src/App.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/divanov11/Appwrite-React-quickstart/658881d728939d622e0f5ac9c4b8eb093a2cbcb2/Part-2/src/App.css -------------------------------------------------------------------------------- /Part-2/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter, Routes, Route } from "react-router-dom"; 2 | import Notes from "./pages/Notes"; 3 | import LoginRegister from "./pages/LoginRegister"; 4 | 5 | function App() { 6 | const selectedTheme = localStorage.getItem("theme"); 7 | 8 | if (selectedTheme) { 9 | document 10 | .querySelector("body") 11 | .setAttribute("data-theme", selectedTheme); 12 | } 13 | 14 | return ( 15 |
16 |
17 | 18 | 19 | } path="/" /> 20 | } path="/login" /> 21 | 22 | 23 |
24 |
25 | ); 26 | } 27 | 28 | export default App; 29 | -------------------------------------------------------------------------------- /Part-2/src/appwrite/config.js: -------------------------------------------------------------------------------- 1 | import { Client, Databases } from "appwrite"; 2 | 3 | const client = new Client(); 4 | 5 | client 6 | .setEndpoint(import.meta.env.VITE_ENDPOINT) 7 | .setProject(import.meta.env.VITE_PROJECT_ID); 8 | 9 | const databases = new Databases(client); 10 | 11 | export { client, databases }; 12 | -------------------------------------------------------------------------------- /Part-2/src/appwrite/databases.js: -------------------------------------------------------------------------------- 1 | import { databases } from "./config"; 2 | import { ID } from "appwrite"; 3 | 4 | const db = {}; 5 | 6 | const collections = [ 7 | { 8 | dbId: import.meta.env.VITE_DATABASE_ID, 9 | id: import.meta.env.VITE_COLLECTION_ID_NOTES, 10 | name: "notes", 11 | }, 12 | ]; 13 | 14 | collections.forEach((col) => { 15 | db[col.name] = { 16 | create: (payload, permissions, id = ID.unique()) => 17 | databases.createDocument( 18 | col.dbId, 19 | col.id, 20 | id, 21 | payload, 22 | permissions 23 | ), 24 | update: (id, payload, permissions) => 25 | databases.updateDocument( 26 | col.dbId, 27 | col.id, 28 | id, 29 | payload, 30 | permissions 31 | ), 32 | delete: (id) => databases.deleteDocument(col.dbId, col.id, id), 33 | 34 | list: (queries = []) => 35 | databases.listDocuments(col.dbId, col.id, queries), 36 | 37 | get: (id) => databases.getDocument(col.dbId, col.id, id), 38 | }; 39 | }); 40 | 41 | export default db; 42 | -------------------------------------------------------------------------------- /Part-2/src/assets/DeleteIcon.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const DeleteIcon = ({ size = 24, stroke = "#f87171" }) => { 4 | return ( 5 | 15 | 16 | 20 | 21 | ); 22 | }; 23 | 24 | export default DeleteIcon; 25 | -------------------------------------------------------------------------------- /Part-2/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Part-2/src/components/Note.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import db from "../appwrite/databases"; 3 | import DeleteIcon from "../assets/DeleteIcon"; 4 | 5 | function Note({ setNotes, noteData }) { 6 | const [note, setNote] = useState(noteData); 7 | 8 | const handleUpdate = async () => { 9 | const completed = !note.completed; 10 | db.notes.update(note.$id, { completed }); 11 | setNote({ ...note, completed: completed }); 12 | }; 13 | 14 | const handleDelete = async () => { 15 | db.notes.delete(note.$id); 16 | setNotes((prevState) => prevState.filter((i) => i.$id !== note.$id)); 17 | }; 18 | 19 | return ( 20 |
21 | 22 | {note.completed ? {note.body} : <>{note.body}} 23 | 24 | 25 |
26 | 27 |
28 |
29 | ); 30 | } 31 | 32 | export default Note; 33 | -------------------------------------------------------------------------------- /Part-2/src/components/NoteForm.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import db from "../appwrite/databases"; 3 | 4 | function NoteForm({ setNotes }) { 5 | const handleAdd = async (e) => { 6 | e.preventDefault(); 7 | const noteBody = e.target.body.value; 8 | 9 | if (noteBody === "") return; 10 | 11 | try { 12 | const payload = { body: noteBody }; 13 | 14 | const response = await db.notes.create(payload); 15 | setNotes((prevState) => [response, ...prevState]); 16 | 17 | e.target.reset(); 18 | } catch (error) { 19 | console.error(error); 20 | } 21 | }; 22 | 23 | return ( 24 |
25 | 30 |
31 | ); 32 | } 33 | 34 | export default NoteForm; 35 | -------------------------------------------------------------------------------- /Part-2/src/components/ThemeOption.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const ThemeOption = ({ theme }) => { 4 | const setTheme = () => { 5 | document.querySelector("body").setAttribute("data-theme", theme); 6 | localStorage.setItem("theme", theme); 7 | }; 8 | 9 | return ( 10 |
15 | ); 16 | }; 17 | 18 | export default ThemeOption; 19 | -------------------------------------------------------------------------------- /Part-2/src/index.css: -------------------------------------------------------------------------------- 1 | :root{ 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-size: 18px; 5 | font-weight: 400; 6 | 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | -webkit-text-size-adjust: 100%; 10 | 11 | 12 | 13 | --primary-background-color: #fff; 14 | --secondary-background-color:#f7f7f7; 15 | 16 | --primary-text-color:#000; 17 | --secondary-text-color:#4b4c53; 18 | 19 | --primary-border-color:#4b4c53; 20 | 21 | --button-solid-background-color:#000; 22 | --button-solid-text-color:#fff; 23 | 24 | --button-outlined-background-color:transparent; 25 | --button-outlined-text-color:#000; 26 | 27 | --alert-color:#FFF9E7; 28 | --alert-border-color:#FFBF00; 29 | --alert-text-color:#b8952f; 30 | } 31 | 32 | [data-theme="dark"]{ 33 | --primary-background-color: #1f2028; 34 | --secondary-background-color:#2e3039; 35 | 36 | --primary-text-color:rgba(255, 255, 255, 0.87); 37 | --secondary-text-color:#4b4c53; 38 | 39 | --primary-border-color:#4b4c53; 40 | 41 | --button-solid-background-color:#fff; 42 | --button-solid-text-color:#000; 43 | 44 | --button-outlined-background-color:transparent; 45 | --button-outlined-text-color:#fff; 46 | 47 | } 48 | 49 | [data-theme="purple"]{ 50 | --primary-background-color: rgba(24, 16, 40, 1); 51 | --secondary-background-color:rgba(44, 36, 70, 1); 52 | 53 | --primary-text-color:rgba(255, 255, 255, 0.87); 54 | --secondary-text-color:#8549a7; 55 | 56 | --primary-border-color:#8549a7; 57 | 58 | --button-solid-background-color:#fff; 59 | --button-solid-text-color:#000; 60 | 61 | --button-outlined-background-color:transparent; 62 | --button-outlined-text-color:#fff; 63 | 64 | } 65 | 66 | 67 | html, body, #root{ 68 | padding: 0; 69 | margin: 0; 70 | height: 100%; 71 | width: 100%; 72 | } 73 | 74 | div{ 75 | overflow-y: auto; 76 | } 77 | 78 | #app{ 79 | height: 100%; 80 | background-color: var(--primary-background-color); 81 | color:var(--primary-text-color); 82 | transition: background-color 0.5s ease, color 0.5s ease; 83 | 84 | } 85 | 86 | #container{ 87 | max-width: 600px; 88 | margin: 0em auto; 89 | padding: 2em 0; 90 | } 91 | 92 | #todo-form{ 93 | margin: 2em 0; 94 | } 95 | 96 | #todo-form input{ 97 | border: 1px solid var(--primary-border-color); 98 | padding: 1em; 99 | border-radius: 10px; 100 | width: 100%; 101 | font-size: 18px; 102 | box-sizing: border-box; 103 | background-color: transparent; 104 | color: var(--primary-text-color); 105 | } 106 | 107 | .note-wrapper{ 108 | /* border-bottom:1px solid hsla(0, 1%,22%,1); */ 109 | background-color: var(--secondary-background-color); 110 | padding: 1em; 111 | margin: 1em 0; 112 | border-radius: 5px; 113 | display: flex; 114 | justify-content: space-between; 115 | /* align-items: center; */ 116 | } 117 | 118 | .note-body{ 119 | cursor: pointer; 120 | } 121 | 122 | .delete{ 123 | cursor: pointer; 124 | } 125 | 126 | .auth-field-wrapper{ 127 | 128 | display: flex; 129 | flex-direction: column; 130 | margin: 1em 0 ; 131 | 132 | 133 | } 134 | 135 | .auth-field-wrapper input[type='text'], input[type='email'], input[type='password']{ 136 | border: none; 137 | outline: none; 138 | background-color: transparent; 139 | height: 75px; 140 | box-sizing: border-box; 141 | border-bottom: 1px solid var(--primary-border-color); 142 | font-size: 18px; 143 | color: var(--button-outlined-text-color); 144 | } 145 | 146 | .auth-field-wrapper input[type='submit']{ 147 | padding: 1em 1.5em; 148 | 149 | width: fit-content; 150 | font-size: 18px; 151 | font-weight: 500; 152 | border:0; 153 | border-radius: 50px; 154 | background-color: var(--button-solid-background-color); 155 | color: var(--button-solid-text-color); 156 | } 157 | 158 | button{ 159 | padding: 1em 1.5em; 160 | 161 | width: fit-content; 162 | font-size: 18px; 163 | font-weight: 500; 164 | border:0; 165 | border-radius: 50px; 166 | border: 1.5px solid var(--button-outlined-text-color); 167 | background-color: transparent; 168 | color: var(--button-outlined-text-color);; 169 | } 170 | 171 | input[type="submit"], button{ 172 | cursor: pointer; 173 | } 174 | 175 | #logout-btn{ 176 | position: fixed; 177 | bottom: 2em; 178 | right: 2em; 179 | } 180 | 181 | .tag{ 182 | background-color:#61b593 ; 183 | padding: 0.5em 1em; 184 | border-radius: 20px; 185 | width: fit-content; 186 | font-size: 12px; 187 | font-weight: 700; 188 | color: #fff; 189 | } 190 | 191 | #pro-header{ 192 | display: flex; 193 | justify-content: space-between; 194 | align-items: center; 195 | } 196 | 197 | #subscribe-prompt{ 198 | display: flex; 199 | align-items:center; 200 | gap: 0.5em; 201 | background-color: var(--alert-color); 202 | border: 1px solid var(--alert-border-color); 203 | padding: 0 1em; 204 | border-radius: 5px; 205 | color: var(--alert-text-color); 206 | cursor: pointer; 207 | transition: 0.3s; 208 | } 209 | 210 | #subscribe-prompt:hover{ 211 | color: #886f23; 212 | background-color: #f0e4c1; 213 | } 214 | 215 | .theme-options{ 216 | display: flex; 217 | gap:10px; 218 | } 219 | 220 | .theme-option{ 221 | height: 25px; 222 | width: 25px; 223 | border: 1px solid; 224 | border-radius: 50%; 225 | cursor: pointer; 226 | } 227 | 228 | #theme-dark{ 229 | background-color: #000; 230 | border-color: #fff; 231 | } 232 | 233 | #theme-light{ 234 | background-color: #fff; 235 | border-color: #000; 236 | } 237 | 238 | #theme-purple{ 239 | background-color: #954ebe; 240 | border-color: #954ebe; 241 | } 242 | 243 | 244 | 245 | 246 | @media screen and (max-width:600px){ 247 | #app{ 248 | padding: 0 1em; 249 | } 250 | } -------------------------------------------------------------------------------- /Part-2/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.jsx' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /Part-2/src/pages/LoginRegister.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function LoginRegister() { 4 | return
LoginRegister
; 5 | } 6 | 7 | export default LoginRegister; 8 | -------------------------------------------------------------------------------- /Part-2/src/pages/Notes.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import db from "../appwrite/databases"; 3 | import NoteForm from "../components/NoteForm"; 4 | import { Query } from "appwrite"; 5 | import Note from "../components/Note"; 6 | import ThemeOption from "../components/ThemeOption"; 7 | 8 | function Notes() { 9 | const [notes, setNotes] = useState([]); 10 | 11 | useEffect(() => { 12 | init(); 13 | }, []); 14 | 15 | const init = async () => { 16 | const response = await db.notes.list([Query.orderDesc("$createdAt")]); 17 | setNotes(response.documents); 18 | }; 19 | 20 | return ( 21 | <> 22 |
23 |

✍️ My Todo List

24 |
25 | 26 |
27 | 28 | 29 | 30 |
31 | 32 | 33 | 34 |
35 | {notes.map((note) => ( 36 | 37 | ))} 38 |
39 | 40 | ); 41 | } 42 | 43 | export default Notes; 44 | -------------------------------------------------------------------------------- /Part-2/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /Part-3/.env.example: -------------------------------------------------------------------------------- 1 | VITE_ENDPOINT=https://cloud.appwrite.io/v1 2 | VITE_PROJECT_ID= 3 | VITE_DATABASE_ID= 4 | VITE_COLLECTION_ID_NOTES= -------------------------------------------------------------------------------- /Part-3/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react-refresh/only-export-components': [ 16 | 'warn', 17 | { allowConstantExport: true }, 18 | ], 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /Part-3/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | .env 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /Part-3/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Part-3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "appwrite-react-quickstart", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "appwrite": "^14.0.1", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0", 16 | "react-router-dom": "^6.23.0" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^18.2.15", 20 | "@types/react-dom": "^18.2.7", 21 | "@vitejs/plugin-react": "^4.0.3", 22 | "eslint": "^8.45.0", 23 | "eslint-plugin-react": "^7.32.2", 24 | "eslint-plugin-react-hooks": "^4.6.0", 25 | "eslint-plugin-react-refresh": "^0.4.3", 26 | "vite": "^4.4.5" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Part-3/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Part-3/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { BrowserRouter, Routes, Route } from "react-router-dom"; 3 | import { account } from "./appwrite/config"; 4 | import Notes from "./pages/Notes"; 5 | import LoginRegister from "./pages/LoginRegister"; 6 | import { PrivateRoutes } from "./components/PrivateRoutes"; 7 | import AuthProvider from "./context/AuthContext"; 8 | 9 | function App() { 10 | return ( 11 |
12 |
13 | 14 | 15 | 16 | } path="/login" /> 17 | }> 18 | } path="/" /> 19 | 20 | 21 | 22 | 23 |
24 |
25 | ); 26 | } 27 | 28 | export default App; 29 | -------------------------------------------------------------------------------- /Part-3/src/appwrite/config.js: -------------------------------------------------------------------------------- 1 | import { Client, Databases, Account } from "appwrite"; 2 | 3 | const client = new Client(); 4 | 5 | client 6 | .setEndpoint(import.meta.env.VITE_ENDPOINT) 7 | .setProject(import.meta.env.VITE_PROJECT_ID); 8 | 9 | const databases = new Databases(client); 10 | const account = new Account(client); 11 | 12 | export { client, databases, account }; 13 | -------------------------------------------------------------------------------- /Part-3/src/appwrite/databases.js: -------------------------------------------------------------------------------- 1 | import { databases } from "./config"; 2 | import { ID } from "appwrite"; 3 | 4 | const db = {}; 5 | 6 | const collections = [ 7 | { 8 | dbId: import.meta.env.VITE_DATABASE_ID, 9 | id: import.meta.env.VITE_COLLECTION_ID_NOTES, 10 | name: "notes", 11 | }, 12 | ]; 13 | 14 | collections.forEach((col) => { 15 | db[col.name] = { 16 | create: (payload, permissions, id = ID.unique()) => 17 | databases.createDocument( 18 | col.dbId, 19 | col.id, 20 | id, 21 | payload, 22 | permissions 23 | ), 24 | update: (id, payload, permissions) => 25 | databases.updateDocument( 26 | col.dbId, 27 | col.id, 28 | id, 29 | payload, 30 | permissions 31 | ), 32 | delete: (id) => databases.deleteDocument(col.dbId, col.id, id), 33 | list: (queries = []) => 34 | databases.listDocuments(col.dbId, col.id, queries), 35 | get: (id) => databases.getDocument(col.dbId, col.id, id), 36 | }; 37 | }); 38 | 39 | export default db; 40 | -------------------------------------------------------------------------------- /Part-3/src/assets/DeleteIcon.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const DeleteIcon = ({ size = 24, stroke = "#f87171" }) => { 4 | return ( 5 | 15 | 16 | 20 | 21 | ); 22 | }; 23 | 24 | export default DeleteIcon; 25 | -------------------------------------------------------------------------------- /Part-3/src/assets/LockIcon.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const LockIcon = () => { 4 | return ( 5 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default LockIcon; 23 | -------------------------------------------------------------------------------- /Part-3/src/assets/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/divanov11/Appwrite-React-quickstart/658881d728939d622e0f5ac9c4b8eb093a2cbcb2/Part-3/src/assets/login.png -------------------------------------------------------------------------------- /Part-3/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Part-3/src/components/LoginForm.jsx: -------------------------------------------------------------------------------- 1 | import { useRef } from "react"; 2 | import { useAuth } from "../context/AuthContext"; 3 | 4 | const LoginForm = ({ setActiveForm }) => { 5 | const { loginUser } = useAuth(); 6 | const formRef = useRef(null); 7 | 8 | const handleLogin = (e) => { 9 | e.preventDefault(); 10 | loginUser(formRef.current.email.value, formRef.current.password.value); 11 | }; 12 | 13 | return ( 14 |
15 |

Login

16 | 17 |
18 |
19 | 20 | 21 |
22 | 23 |
24 | 25 | 26 |
27 | 28 |
29 | 30 |
31 |
32 | 33 |

34 | Don't have an account?{" "} 35 | { 38 | setActiveForm("register"); 39 | }} 40 | > 41 | Signup 42 | 43 |

44 |
45 | ); 46 | }; 47 | 48 | export default LoginForm; 49 | -------------------------------------------------------------------------------- /Part-3/src/components/LogoutButton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useAuth } from "../context/AuthContext"; 3 | 4 | const LogoutButton = () => { 5 | const { logoutUser } = useAuth(); 6 | 7 | return ( 8 | 11 | ); 12 | }; 13 | 14 | export default LogoutButton; 15 | -------------------------------------------------------------------------------- /Part-3/src/components/Note.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import DeleteIcon from "../assets/DeleteIcon"; 3 | import db from "../appwrite/databases"; 4 | 5 | const Note = ({ noteData, setNotes }) => { 6 | const [note, setNote] = useState(noteData); 7 | 8 | useEffect(() => {}, [note]); 9 | 10 | const handleDelete = async () => { 11 | db.notes.delete(note.$id); 12 | setNotes((prevState) => prevState.filter((i) => i.$id !== note.$id)); 13 | }; 14 | 15 | const handleUpdate = async () => { 16 | const completed = !note.completed; 17 | db.notes.update(note.$id, { completed }); 18 | setNote({ ...note, completed: completed }); 19 | }; 20 | return ( 21 |
22 | 23 | {note.completed ? {note.body} : <>{note.body}} 24 | 25 | 26 |
27 | 28 |
29 |
30 | ); 31 | }; 32 | 33 | export default Note; 34 | -------------------------------------------------------------------------------- /Part-3/src/components/NoteForm.jsx: -------------------------------------------------------------------------------- 1 | import db from "../appwrite/databases"; 2 | 3 | const NoteForm = ({ setNotes }) => { 4 | const handleAdd = async (e) => { 5 | e.preventDefault(); 6 | const noteBody = e.target.body.value; 7 | if (noteBody === "") return; 8 | 9 | try { 10 | const payload = { 11 | body: noteBody, 12 | }; 13 | 14 | const response = await db.notes.create(payload); 15 | setNotes((prevState) => [response, ...prevState]); 16 | 17 | e.target.reset(); 18 | } catch (error) { 19 | console.error(error); 20 | } 21 | }; 22 | 23 | return ( 24 |
25 | 30 |
31 | ); 32 | }; 33 | 34 | export default NoteForm; 35 | -------------------------------------------------------------------------------- /Part-3/src/components/PrivateRoutes.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Navigate, Outlet } from "react-router-dom"; 3 | import { useAuth } from "../context/AuthContext"; 4 | 5 | export const PrivateRoutes = (props) => { 6 | const { user } = useAuth(); 7 | return user ? : ; 8 | }; 9 | -------------------------------------------------------------------------------- /Part-3/src/components/RegisterFrom.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const RegisterFrom = ({ setActiveForm }) => { 4 | return ( 5 |
6 |

Register

7 |
8 |
9 | 10 | 11 |
12 | 13 |
14 | 15 | 16 |
17 | 18 |
19 | 20 | 21 |
22 | 23 |
24 | 25 |
26 |
27 | 28 |

29 | Already have an account?{" "} 30 | { 33 | setActiveForm("login"); 34 | }} 35 | > 36 | Login 37 | 38 |

39 |
40 | ); 41 | }; 42 | 43 | export default RegisterFrom; 44 | -------------------------------------------------------------------------------- /Part-3/src/components/ThemeOption.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const ThemeOption = ({ bg, border, theme }) => { 4 | const changeTheme = (theme) => { 5 | console.log("Change theme"); 6 | document.getElementById("app").setAttribute("data-theme", theme); 7 | }; 8 | 9 | return ( 10 |
changeTheme(theme)} 12 | className="theme-option" 13 | style={{ backgroundColor: bg, borderColor: border }} 14 | >
15 | ); 16 | }; 17 | 18 | export default ThemeOption; 19 | -------------------------------------------------------------------------------- /Part-3/src/context/AuthContext.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, createContext, useContext } from "react"; 2 | import { account } from "../appwrite/config"; 3 | 4 | const AuthContext = createContext(); 5 | 6 | const AuthProvider = ({ children }) => { 7 | const [loading, setLoading] = useState(true); 8 | const [user, setUser] = useState(null); 9 | 10 | useEffect(() => { 11 | init(); 12 | }, []); 13 | 14 | const init = async () => { 15 | const response = await checkUserStatus(); 16 | setUser(response); 17 | setLoading(false); 18 | }; 19 | 20 | const checkUserStatus = async () => { 21 | try { 22 | const userData = await account.get(); 23 | return userData; 24 | } catch (error) { 25 | console.error(error); 26 | return null; 27 | } 28 | }; 29 | 30 | const loginUser = async (email, password) => { 31 | setLoading(true); 32 | try { 33 | await account.createEmailPasswordSession(email, password); 34 | const response = await checkUserStatus(); 35 | setUser(response); 36 | } catch (error) { 37 | console.error(error); 38 | } 39 | setLoading(false); 40 | }; 41 | 42 | const logoutUser = async () => { 43 | await account.deleteSession("current"); 44 | setUser(null); 45 | }; 46 | 47 | const contextData = { user, loginUser, logoutUser }; 48 | 49 | return ( 50 | 51 | {loading ?

Loading...

: children} 52 |
53 | ); 54 | }; 55 | 56 | const useAuth = () => { 57 | return useContext(AuthContext); 58 | }; 59 | 60 | export { useAuth }; 61 | 62 | export default AuthProvider; 63 | -------------------------------------------------------------------------------- /Part-3/src/index.css: -------------------------------------------------------------------------------- 1 | :root{ 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-size: 18px; 5 | font-weight: 400; 6 | 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | -webkit-text-size-adjust: 100%; 10 | 11 | 12 | 13 | --primary-background-color: #fff; 14 | --secondary-background-color:#f7f7f7; 15 | 16 | --primary-text-color:#000; 17 | --secondary-text-color:#4b4c53; 18 | 19 | --primary-border-color:#4b4c53; 20 | 21 | --button-solid-background-color:#000; 22 | --button-solid-text-color:#fff; 23 | 24 | --button-outlined-background-color:transparent; 25 | --button-outlined-text-color:#000; 26 | 27 | --alert-color:#FFF9E7; 28 | --alert-border-color:#FFBF00; 29 | --alert-text-color:#b8952f; 30 | } 31 | 32 | [data-theme="dark"]{ 33 | --primary-background-color: #1f2028; 34 | --secondary-background-color:#2e3039; 35 | 36 | --primary-text-color:rgba(255, 255, 255, 0.87); 37 | --secondary-text-color:#4b4c53; 38 | 39 | --primary-border-color:#4b4c53; 40 | 41 | --button-solid-background-color:#fff; 42 | --button-solid-text-color:#000; 43 | 44 | --button-outlined-background-color:transparent; 45 | --button-outlined-text-color:#fff; 46 | 47 | } 48 | 49 | [data-theme="purple"]{ 50 | --primary-background-color: rgba(24, 16, 40, 1); 51 | --secondary-background-color:rgba(44, 36, 70, 1); 52 | 53 | --primary-text-color:rgba(255, 255, 255, 0.87); 54 | --secondary-text-color:#8549a7; 55 | 56 | --primary-border-color:#8549a7; 57 | 58 | --button-solid-background-color:#fff; 59 | --button-solid-text-color:#000; 60 | 61 | --button-outlined-background-color:transparent; 62 | --button-outlined-text-color:#fff; 63 | 64 | } 65 | 66 | 67 | html, body, #root{ 68 | padding: 0; 69 | margin: 0; 70 | height: 100%; 71 | width: 100%; 72 | } 73 | 74 | div{ 75 | overflow-y: auto; 76 | } 77 | 78 | #app{ 79 | height: 100%; 80 | background-color: var(--primary-background-color); 81 | color:var(--primary-text-color); 82 | transition: background-color 0.5s ease, color 0.5s ease; 83 | 84 | } 85 | 86 | #container{ 87 | max-width: 600px; 88 | margin: 0em auto; 89 | padding: 2em 0; 90 | } 91 | 92 | #todo-form{ 93 | margin: 2em 0; 94 | } 95 | 96 | #todo-form input{ 97 | border: 1px solid var(--primary-border-color); 98 | padding: 1em; 99 | border-radius: 10px; 100 | width: 100%; 101 | font-size: 18px; 102 | box-sizing: border-box; 103 | background-color: transparent; 104 | color: var(--primary-text-color); 105 | } 106 | 107 | .note-wrapper{ 108 | /* border-bottom:1px solid hsla(0, 1%,22%,1); */ 109 | background-color: var(--secondary-background-color); 110 | padding: 1em; 111 | margin: 1em 0; 112 | border-radius: 5px; 113 | display: flex; 114 | justify-content: space-between; 115 | /* align-items: center; */ 116 | } 117 | 118 | .note-body{ 119 | cursor: pointer; 120 | } 121 | 122 | .delete{ 123 | cursor: pointer; 124 | } 125 | 126 | .auth-field-wrapper{ 127 | 128 | display: flex; 129 | flex-direction: column; 130 | margin: 1em 0 ; 131 | 132 | 133 | } 134 | 135 | .auth-field-wrapper input[type='text'], input[type='email'], input[type='password']{ 136 | border: none; 137 | outline: none; 138 | background-color: transparent; 139 | height: 75px; 140 | box-sizing: border-box; 141 | border-bottom: 1px solid var(--primary-border-color); 142 | font-size: 18px; 143 | color: var(--button-outlined-text-color); 144 | } 145 | 146 | .auth-field-wrapper input[type='submit']{ 147 | padding: 1em 1.5em; 148 | 149 | width: fit-content; 150 | font-size: 18px; 151 | font-weight: 500; 152 | border:0; 153 | border-radius: 50px; 154 | background-color: var(--button-solid-background-color); 155 | color: var(--button-solid-text-color); 156 | } 157 | 158 | button{ 159 | padding: 1em 1.5em; 160 | 161 | width: fit-content; 162 | font-size: 18px; 163 | font-weight: 500; 164 | border:0; 165 | border-radius: 50px; 166 | border: 1.5px solid var(--button-outlined-text-color); 167 | background-color: transparent; 168 | color: var(--button-outlined-text-color);; 169 | } 170 | 171 | input[type="submit"], button{ 172 | cursor: pointer; 173 | } 174 | 175 | #logout-btn{ 176 | position: fixed; 177 | bottom: 2em; 178 | right: 2em; 179 | } 180 | 181 | .tag{ 182 | background-color:#61b593 ; 183 | padding: 0.5em 1em; 184 | border-radius: 20px; 185 | width: fit-content; 186 | font-size: 12px; 187 | font-weight: 700; 188 | color: #fff; 189 | } 190 | 191 | #pro-header{ 192 | display: flex; 193 | justify-content: space-between; 194 | align-items: center; 195 | } 196 | 197 | #subscribe-prompt{ 198 | display: flex; 199 | align-items:center; 200 | gap: 0.5em; 201 | background-color: var(--alert-color); 202 | border: 1px solid var(--alert-border-color); 203 | padding: 0 1em; 204 | border-radius: 5px; 205 | color: var(--alert-text-color); 206 | cursor: pointer; 207 | transition: 0.3s; 208 | } 209 | 210 | #subscribe-prompt:hover{ 211 | color: #886f23; 212 | background-color: #f0e4c1; 213 | } 214 | 215 | .theme-options{ 216 | display: flex; 217 | gap:10px; 218 | } 219 | 220 | .theme-option{ 221 | height: 25px; 222 | width: 25px; 223 | border: 1px solid; 224 | border-radius: 50%; 225 | cursor: pointer; 226 | } 227 | 228 | 229 | 230 | @media screen and (max-width:600px){ 231 | #app{ 232 | padding: 0 1em; 233 | } 234 | } -------------------------------------------------------------------------------- /Part-3/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.jsx' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /Part-3/src/pages/LoginRegister.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | import { useAuth } from "../context/AuthContext"; 4 | import LoginForm from "../components/LoginForm"; 5 | import RegisterFrom from "../components/RegisterFrom"; 6 | 7 | const LoginRegister = () => { 8 | const navigate = useNavigate(); 9 | const { user } = useAuth(); 10 | const [activeForm, setActiveForm] = useState("login"); 11 | 12 | useEffect(() => { 13 | if (user) { 14 | navigate("/"); 15 | } 16 | }); 17 | 18 | return activeForm === "login" ? ( 19 | 20 | ) : ( 21 | 22 | ); 23 | }; 24 | export default LoginRegister; 25 | -------------------------------------------------------------------------------- /Part-3/src/pages/Notes.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Query } from "appwrite"; 3 | import { Navigate } from "react-router-dom"; 4 | import db from "../appwrite/databases"; 5 | import NoteForm from "../components/NoteForm"; 6 | import Note from "../components/Note"; 7 | import LogoutButton from "../components/LogoutButton"; 8 | import { useAuth } from "../context/AuthContext"; 9 | import ThemeOption from "../components/ThemeOption"; 10 | import LockIcon from "../assets/LockIcon"; 11 | 12 | const Notes = () => { 13 | const [notes, setNotes] = useState([]); 14 | const [pro, setPro] = useState(false); 15 | const { user } = useAuth(); 16 | 17 | useEffect(() => { 18 | init(); 19 | }, []); 20 | 21 | const init = async () => { 22 | setPro(user.labels.includes("premium")); 23 | const response = await db.notes.list([Query.orderDesc("$createdAt")]); 24 | setNotes(response.documents); 25 | }; 26 | 27 | return ( 28 | <> 29 | {/* {pro ? ( 30 |
31 |
32 | Pro member 33 |
34 |
35 | 40 | 41 | 42 |
43 |
44 | ) : ( 45 |
46 | 47 |

Unlock purple mode for $1

48 |
49 | )} */} 50 | 51 |
52 |
53 | Pro member 54 |
55 |
56 | 57 | 58 | 59 |
60 |
61 |
62 |

✍️ My Todo List

63 |
64 | 65 | 66 | 67 | {notes.map((note) => ( 68 | 69 | ))} 70 | 71 | 72 | 73 | ); 74 | }; 75 | 76 | export default Notes; 77 | -------------------------------------------------------------------------------- /Part-3/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Appwrite 🤝 React 2 | 3 | Appwrite + React fullstack todo app with integrated auth. 4 | 5 | ## Understanding Folder Structure 6 | 7 | This project is part of a multi part video series, so our folders are structured in a way to represent the code at the end of each video. i.e. `Video 1` == `Part 1`, and so on. 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
PartTopicDescriptionView
1CRUDBasic setup and CRUD with Appwrite + ReactView
2Theme SwitcherAdded theme switcherView
2Theme Switcher (Template Only)Theme switcher with no backend.View
3AuthenticationLogin, Logout, Registration & Protected RoutesView
41 | 42 | See tutorial part 1 here: https://youtu.be/_JDeJgsU-bI 43 | 44 | 45 | 46 | ## Setup instructions 47 | 48 | Before you can clone and setup a local instance of this project you'll need to setup an Appwrite backend and gain the nessesary credentials + have the correct database setup and permisisons configured. 49 | 50 | ### Setting up appwrite backend 51 | 52 | 1. Create a new appwrite project + app. Easiest way to get started is by heading to [appwrite.io](https://appwrite.io/) 53 | 54 | 2. Add a platform and set hostname registration 55 | 56 | This can be done by going to `overview` -> `Add Platform` -> `Web App`. 57 | 58 | 3. Add a user 59 | 60 | 4. Add a database and a collection 61 | 62 | Add the following attributes in your collection 63 | 64 |
65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 |
NameTypeDetails
bodystring100 chars
completedbooleanDefault to false
81 | 82 | 5. Set collection perissions 83 | 84 | This can be done under from your collection tab unde `settings` -> `permissions`. 85 | 86 | Set permissions to `users` + `create` 87 | 88 | Enable document level security 89 | 90 | #### Setup local repo 91 | 92 | **Clone Repo** 93 | Clone repo: `git clone ` 94 | 95 | Add `.env` file and fill in your Appwrite project credentials: 96 | 97 | ``` 98 | VITE_PROJECT_ID=YOUR-PROJECT-ID 99 | VITE_DATABASE_ID=YOUR-DATABASE-ID 100 | VITE_COLLECTION_TASKS=YOUR-COLLECTION-ID 101 | ``` 102 | 103 | - `cd ` 104 | - `npm i` && `npm run dev` 105 | -------------------------------------------------------------------------------- /assets/.env.example: -------------------------------------------------------------------------------- 1 | VITE_ENDPOINT=https://cloud.appwrite.io/v1 2 | VITE_PROJECT_ID= 3 | VITE_DATABASE_ID= 4 | VITE_COLLECTION_ID_NOTES= -------------------------------------------------------------------------------- /assets/notes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/divanov11/Appwrite-React-quickstart/658881d728939d622e0f5ac9c4b8eb093a2cbcb2/assets/notes.png -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Appwrite-React-quickstart", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | --------------------------------------------------------------------------------