├── .eslintrc.cjs ├── .gitattributes ├── .gitignore ├── README.md ├── index.html ├── netlify.toml ├── package-lock.json ├── package.json ├── postcss.config.js ├── public └── vite.svg ├── src ├── App.tsx ├── assets │ └── react.svg ├── components │ ├── AddTask.tsx │ ├── Button.tsx │ ├── Dialog.tsx │ ├── Navbar.tsx │ ├── Search.tsx │ ├── Select.tsx │ ├── Speaker.tsx │ └── TaskItem.tsx ├── hooks │ └── useSpeechToTextHelper.ts ├── index.css ├── main.tsx ├── models │ └── interface.ts ├── routes │ ├── Index.tsx │ └── Task.tsx ├── txt ├── utils │ ├── ai.ts │ ├── appwrite.ts │ ├── db.ts │ └── shared.ts └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.env 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | pnpm-debug.log* 9 | lerna-debug.log* 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # task-app-appwrite 2 | 3 | This is a task app that uses Appwrite as a backend. 4 | Live URL: [https://taskwrite.netlify.app/](https://task-app-appwrite.netlify.app/) 5 | 6 | 7 | 8 | **Features - MVE(xample)** 9 | 10 | - [x] User can create, read, update and delete a task 11 | 12 | - [x] User can mark a task as completed 13 | 14 | - [x] User can set task due date 15 | 16 | - [x] Add a simple UI 17 | 18 | **Features - Nice to haves** 19 | 20 | - [x] User can set task priority 21 | 22 | - [x] User can sort tasks 23 | 24 | ~~- [ ] Tasks are draggable~~ 25 | 26 | - [x] Support Dark Mode 27 | 28 | - [x] AI can generate task descriptions based on task title 29 | 30 | - [x] Tasks are searchable 31 | 32 | - [x] User can add tasks via voice commands 33 | 34 | ~~- [ ] User can get push notifications when tasks are almost due~~ 35 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/*" 3 | to = "/index.html" 4 | status = 200 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "task-app", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@heroicons/react": "^2.1.1", 14 | "@huggingface/inference": "^2.6.4", 15 | "@types/react-speech-recognition": "^3.9.5", 16 | "ai": "^2.2.36", 17 | "appwrite": "^13.0.2", 18 | "react": "^18.2.0", 19 | "react-dom": "^18.2.0", 20 | "react-router-dom": "^6.22.2", 21 | "react-speech-recognition": "^3.10.0", 22 | "regenerator-runtime": "^0.14.1" 23 | }, 24 | "devDependencies": { 25 | "@types/react": "^18.2.43", 26 | "@types/react-dom": "^18.2.17", 27 | "@typescript-eslint/eslint-plugin": "^6.14.0", 28 | "@typescript-eslint/parser": "^6.14.0", 29 | "@vitejs/plugin-react": "^4.2.1", 30 | "autoprefixer": "^10.4.17", 31 | "eslint": "^8.55.0", 32 | "eslint-plugin-react-hooks": "^4.6.0", 33 | "eslint-plugin-react-refresh": "^0.4.5", 34 | "postcss": "^8.4.35", 35 | "tailwindcss": "^3.4.1", 36 | "typescript": "^5.2.2", 37 | "vite": "^5.0.8" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | import { Route, Routes } from "react-router-dom"; 3 | import Task from "./routes/Task"; 4 | import Index from "./routes/Index"; 5 | import Navbar from "./components/Navbar"; 6 | 7 | function App() { 8 | return ( 9 | <> 10 | 11 | 12 | } /> 13 | } /> 14 | 15 | 16 | ); 17 | } 18 | 19 | export default App; 20 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/AddTask.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import Select from "./Select"; 3 | import Button from "./Button"; 4 | import { createDocument, updateDocument } from "../utils/db"; 5 | import { useNavigate } from "react-router-dom"; 6 | import { IPayload, ITask } from "../models/interface"; 7 | import { getTasks } from "../utils/shared"; 8 | import { callAI } from "../utils/ai"; 9 | import { SparklesIcon } from "@heroicons/react/24/solid"; 10 | import Speaker from "./Speaker"; 11 | import { useSpeechToTextHelper } from "../hooks/useSpeechToTextHelper"; 12 | 13 | interface ITaskFormProps { 14 | task: ITask | null; 15 | isEdit?: boolean; 16 | setTasks?: (tasks: ITask[]) => void; 17 | } 18 | 19 | const AddTask = ({ task, isEdit, setTasks }: ITaskFormProps) => { 20 | const navigate = useNavigate(); 21 | const { transcript, resetTranscript } = useSpeechToTextHelper(); 22 | 23 | const [titleVal, setTitleVal] = useState(""); 24 | const [textAreaVal, setTextAreaVal] = useState(""); 25 | const [dueDate, setDueDate] = useState( 26 | isEdit && task?.due_date ? new Date(task.due_date) : new Date() 27 | ); 28 | 29 | const [isSubmitting, setIsSubmitting] = useState(false); 30 | const [isGenerating, setIsGenerating] = useState(false); 31 | 32 | const priorityArray = ["low", "medium", "high"]; 33 | 34 | const [priority, setPriority] = useState( 35 | isEdit && task?.priority ? task?.priority : priorityArray[0] 36 | ); 37 | 38 | const [titleValidationError, setTitleValidationError] = useState(""); 39 | 40 | useEffect(() => { 41 | if (isEdit && task && !transcript) { 42 | setTitleVal(task.title); 43 | setTextAreaVal(task.description); 44 | } else { 45 | setTitleVal(transcript || ""); 46 | } 47 | }, [isEdit, task, transcript]); 48 | 49 | const handleTitleChange = (e: React.ChangeEvent) => { 50 | setTitleVal(e.target.value); 51 | 52 | if (e.target.value.trim() !== "") { 53 | setTitleValidationError(""); 54 | } 55 | }; 56 | 57 | const clearTranscript = () => { 58 | resetTranscript(); 59 | }; 60 | 61 | const handleSubmitTask = async (e: React.FormEvent) => { 62 | e.preventDefault(); 63 | setIsSubmitting(true); 64 | 65 | try { 66 | if (!titleVal) { 67 | setTitleValidationError("Please provide at least a title for the task"); 68 | setTimeout(() => setTitleValidationError(""), 2000); 69 | setIsSubmitting(false); 70 | return; 71 | } 72 | 73 | if (titleVal.length > 49) { 74 | setTitleValidationError( 75 | "Title too long. It can only be 49 characters long" 76 | ); 77 | setTimeout(() => setTitleValidationError(""), 2000); 78 | setIsSubmitting(false); 79 | return; 80 | } 81 | 82 | const payload: IPayload = { 83 | title: titleVal, 84 | description: textAreaVal, 85 | due_date: dueDate, 86 | priority: priority, 87 | }; 88 | 89 | if (isEdit && task) { 90 | await updateDocument(payload, task!.$id); 91 | const allTasks = await getTasks(); 92 | if (setTasks) return setTasks(allTasks.reverse()); 93 | } else { 94 | await createDocument(payload); 95 | } 96 | 97 | // reset form 98 | setTitleVal(""); 99 | setTextAreaVal(""); 100 | setDueDate(new Date()); 101 | setPriority(priorityArray[0]); 102 | setTitleValidationError(""); 103 | setIsSubmitting(false); 104 | navigate("/tasks"); 105 | } catch (error) { 106 | console.error("Error in handleSubmitTask:", error); 107 | setIsSubmitting(false); 108 | } 109 | }; 110 | 111 | const generateDesc = async () => { 112 | setTextAreaVal(""); 113 | 114 | if (!titleVal) { 115 | alert("Please provide a title for the task"); 116 | return; 117 | } 118 | setIsGenerating(true); 119 | 120 | const prompt = `Provide a description for this task: ${titleVal}. Keep the description to a maximum of 30 words. Do NOT include a leading "Task" in the description`; 121 | 122 | try { 123 | const res = await callAI(prompt); 124 | const responseText = res.generated_text; 125 | 126 | setIsGenerating(false); 127 | 128 | responseText.split("").forEach((char, index) => { 129 | setTimeout(() => { 130 | setTextAreaVal((prevText) => prevText + char); 131 | }, index * 32); 132 | }); 133 | } catch (error) { 134 | console.log("ERROR HUGGING FACE API: " + error); 135 | } 136 | }; 137 | 138 | return ( 139 |
140 |
141 |
142 | 143 | 144 |
145 | 157 | {titleValidationError && ( 158 | {titleValidationError} 159 | )} 160 |
161 |
162 | 165 |