├── .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 |
224 | );
225 | };
226 |
227 | export default AddTask;
228 |
--------------------------------------------------------------------------------
/src/components/Button.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 |
3 | interface ButtonProps {
4 | extraBtnClasses?: string;
5 | textColor?: string;
6 | handleClick?: (e: React.MouseEvent) => void;
7 | title?: string;
8 | disable?: boolean;
9 | type?: "button" | "submit" | "reset";
10 | children: ReactNode;
11 | }
12 |
13 | function Button({
14 | extraBtnClasses,
15 | textColor,
16 | handleClick,
17 | title,
18 | disable,
19 | type = "button",
20 | children,
21 | }: ButtonProps) {
22 | const handleClickProp = type === "submit" ? undefined : handleClick;
23 |
24 | return (
25 |
36 | );
37 | }
38 |
39 | export default Button;
40 |
--------------------------------------------------------------------------------
/src/components/Dialog.tsx:
--------------------------------------------------------------------------------
1 | import { XMarkIcon } from "@heroicons/react/24/solid";
2 | import { ReactNode, useState } from "react";
3 | import Button from "./Button";
4 | import { ITask } from "../models/interface";
5 |
6 | interface DialogProps {
7 | setIsViewTask?: (isViewTask: boolean) => void;
8 | setSearchedTasks?: (tasks: ITask[]) => void;
9 | children: ReactNode;
10 | }
11 |
12 | function Dialog({ setIsViewTask, setSearchedTasks, children }: DialogProps) {
13 | const [isOpen, setIsOpen] = useState(true);
14 |
15 | const closeModal = () => {
16 | if (setIsViewTask) setIsViewTask(false);
17 | if (setSearchedTasks) setSearchedTasks([]);
18 | setIsOpen(false);
19 | };
20 | return (
21 |
41 | );
42 | }
43 |
44 | export default Dialog;
45 |
--------------------------------------------------------------------------------
/src/components/Navbar.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 | import Select from "./Select";
3 | import Button from "./Button";
4 | import { PencilIcon } from "@heroicons/react/24/solid";
5 | import { useEffect, useState } from "react";
6 |
7 | const Navbar = () => {
8 | const themeArray = ["light", "dark", "system"];
9 | const [theme, setTheme] = useState(() => {
10 | return localStorage.getItem("theme") || themeArray[2];
11 | });
12 |
13 | const applyTheme = (selectedTheme: string) => {
14 | const isDarkModePreferred = window.matchMedia(
15 | "(prefers-color-scheme: dark)"
16 | ).matches;
17 |
18 | document.documentElement.classList.remove("light", "dark");
19 | document.documentElement.classList.add(selectedTheme);
20 |
21 | if (selectedTheme === "system") {
22 | document.documentElement.classList.toggle("dark", isDarkModePreferred);
23 | document.documentElement.classList.toggle("light", !isDarkModePreferred);
24 | }
25 | };
26 |
27 | const handleSelectTheme = (e: React.ChangeEvent) => {
28 | const selectedTheme = e.target.value;
29 | setTheme(selectedTheme);
30 |
31 | // Store the selected theme in localStorage
32 | localStorage.setItem("theme", selectedTheme);
33 | };
34 |
35 | useEffect(() => {
36 | applyTheme(theme);
37 | }, [theme]);
38 |
39 | return (
40 |
66 | );
67 | };
68 |
69 | export default Navbar;
70 |
--------------------------------------------------------------------------------
/src/components/Search.tsx:
--------------------------------------------------------------------------------
1 | import { FormEvent, useState } from "react";
2 | import { ITask } from "../models/interface";
3 | import Dialog from "./Dialog";
4 | import TaskItem from "./TaskItem";
5 | import Button from "./Button";
6 | import { searchTasks } from "../utils/db";
7 |
8 | const Search = () => {
9 | const [searchTerm, setSearchTerm] = useState("");
10 | const [isSearching, setIsSearching] = useState(false);
11 | const [searchedTasks, setSearchedTasks] = useState([]);
12 | const [error, setError] = useState("");
13 |
14 | const handleSubmit = async (e: FormEvent) => {
15 | e.preventDefault();
16 | if (!searchTerm) {
17 | setError("No search term entered");
18 | setTimeout(() => {
19 | setError("");
20 | }, 3000);
21 | return;
22 | }
23 |
24 | setIsSearching(true);
25 |
26 | const res = await searchTasks(searchTerm);
27 | console.log("res search: ", res);
28 | if (res.length === 0) {
29 | setIsSearching(false);
30 | setError("No task found");
31 | setTimeout(() => {
32 | setSearchTerm("");
33 | setError("");
34 | }, 3000);
35 | return;
36 | }
37 | setIsSearching(false);
38 | setSearchedTasks(res as ITask[]);
39 | };
40 | return (
41 |
42 |
73 | {error}
74 |
75 | );
76 | };
77 |
78 | export default Search;
79 |
--------------------------------------------------------------------------------
/src/components/Select.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | interface SelectProps {
4 | defaultSelectValue: string;
5 | selectOptions: string[];
6 | handleSelectChange: (e: React.ChangeEvent) => void;
7 | }
8 |
9 | const Select = ({
10 | defaultSelectValue,
11 | handleSelectChange,
12 | selectOptions,
13 | }: SelectProps) => {
14 | const [selectVal, setSelectVal] = useState(defaultSelectValue);
15 | return (
16 |
30 | );
31 | };
32 |
33 | export default Select;
34 |
--------------------------------------------------------------------------------
/src/components/Speaker.tsx:
--------------------------------------------------------------------------------
1 | import { useSpeechToTextHelper } from "../hooks/useSpeechToTextHelper";
2 | import { MicrophoneIcon, XCircleIcon } from "@heroicons/react/24/solid";
3 | import Button from "./Button";
4 | import SpeechRecognition from "react-speech-recognition";
5 |
6 | interface SpeakerProps {
7 | handleClear: (e: React.MouseEvent) => void;
8 | }
9 |
10 | function Speaker({ handleClear }: SpeakerProps) {
11 | const { listening, error } = useSpeechToTextHelper();
12 |
13 | const handleSpeech = () => {
14 | SpeechRecognition.startListening();
15 | };
16 |
17 | return (
18 |
19 | {error &&
{error}
}
20 |
21 | {listening ? "Mic on" : "Mic off"}
22 |
29 |
37 |
38 |
39 | );
40 | }
41 |
42 | export default Speaker;
43 |
--------------------------------------------------------------------------------
/src/components/TaskItem.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import Button from "./Button";
3 | import { PencilSquareIcon, TrashIcon } from "@heroicons/react/24/solid";
4 | import { IPayload, ITask } from "../models/interface";
5 | import { deleteDocument, updateDocument } from "../utils/db";
6 | import { getTasks } from "../utils/shared";
7 | import { useNavigate } from "react-router-dom";
8 |
9 | interface TaskItemProps {
10 | task: ITask;
11 | setTasks?: (tasks: ITask[]) => void;
12 | isViewTask: boolean;
13 | handleViewTask?: (e: React.MouseEvent) => void;
14 | }
15 |
16 | function TaskItem({
17 | task,
18 | setTasks,
19 | isViewTask = false,
20 | handleViewTask,
21 | }: TaskItemProps) {
22 | const navigate = useNavigate();
23 |
24 | const [isDone, setIsDone] = useState(false);
25 |
26 | const updateTasks = async () => {
27 | try {
28 | const allTasks = await getTasks();
29 | if (setTasks) setTasks(allTasks.reverse());
30 | } catch (error) {
31 | console.error(error);
32 | }
33 | };
34 |
35 | const handleEdit = async (currentTask: ITask) => {
36 | navigate("/", { state: { task: currentTask } });
37 | };
38 |
39 | const handleDelete = async (
40 | e: React.MouseEvent,
41 | currentTaskId: string
42 | ) => {
43 | e.stopPropagation();
44 | try {
45 | await deleteDocument(currentTaskId);
46 | if (isViewTask) {
47 | navigate(0);
48 | } else {
49 | updateTasks();
50 | }
51 | } catch (error) {
52 | console.error(error);
53 | }
54 | };
55 |
56 | const handleCheckbox = async (
57 | currentTask: IPayload,
58 | id: string,
59 | e: React.ChangeEvent
60 | ) => {
61 | const payload: IPayload = {
62 | title: currentTask.title,
63 | description: currentTask.description,
64 | due_date: currentTask.due_date,
65 | priority: currentTask.priority,
66 | done: e.target.checked,
67 | };
68 |
69 | try {
70 | await updateDocument(payload, id);
71 | updateTasks();
72 | } catch (error) {
73 | console.error(error);
74 | }
75 | };
76 |
77 | return (
78 | <>
79 |
83 |
87 |
88 | {task.priority && (
89 |
90 | Priority:
91 |
100 | {task.priority}
101 |
102 |
103 | )}
104 |
105 | {!task.done && (
106 |
113 | )}
114 |
121 |
122 |
123 |
124 |
125 | {task.title}
126 |
127 |
128 | {task.description.length > 70 && !isViewTask
129 | ? task.description.substring(0, 70) + "..."
130 | : task.description}
131 |
132 |
133 | Due on:
134 | {`${new Date(
135 | task.due_date
136 | ).toLocaleDateString()}`}
137 |
138 |
139 |
162 |
163 |
164 | >
165 | );
166 | }
167 |
168 | export default TaskItem;
169 |
--------------------------------------------------------------------------------
/src/hooks/useSpeechToTextHelper.ts:
--------------------------------------------------------------------------------
1 | import "regenerator-runtime/runtime";
2 | import { useState } from "react";
3 | import { useSpeechRecognition } from "react-speech-recognition";
4 |
5 | export function useSpeechToTextHelper() {
6 | const [error, setError] = useState("");
7 |
8 | const {
9 | transcript,
10 | listening,
11 | resetTranscript,
12 | browserSupportsSpeechRecognition,
13 | } = useSpeechRecognition();
14 |
15 | if (!browserSupportsSpeechRecognition) {
16 | setError("Browser doesn't support speech recognition.");
17 | }
18 |
19 | return {
20 | error,
21 | listening,
22 | transcript,
23 | resetTranscript,
24 | };
25 | }
26 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&family=Quicksand:wght@300..700&display=swap');
2 | @tailwind base;
3 | @tailwind components;
4 | @tailwind utilities;
5 |
6 | @layer base {
7 | :root{
8 | --base-bg: #ffffff;
9 | --btn-bg-primary: #be185d;
10 | --btn-bg-primary-hover: #9d174d;
11 | --btn-icon-main: #1e293b;
12 | --btn-bg-ok: #4ade80;
13 | --btn-bg-light-ok: #bbf7d0;
14 | --btn-bg-light: #e5e7eb;
15 | --low-priority: #facc15;
16 | --medium-priority: #fb923c;
17 | --high-priority: #f87171;
18 | --text-error: #dc2626;
19 | --text-ok: #16a34a;
20 | --text-main: #262626;
21 | --border-container: #9ca3af;
22 | --border-input: #1e293b;
23 | --border-error: #dc2626;
24 | }
25 |
26 | body{
27 | background-color: var(--base-bg);
28 | color: var(--text-main);
29 | }
30 |
31 | #date::-webkit-calendar-picker-indicator {
32 | background-color: var(--btn-bg-light);
33 | }
34 |
35 | .dark{
36 | --base-bg: #262626;
37 | --text-main: #ffffff;
38 | --text-error: #fca5a5;
39 | --text-ok: #86efac;
40 | --border-input: #e2e8f0;
41 | --border-error: #fca5a5;
42 | }
43 | }
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import { BrowserRouter } from "react-router-dom";
4 | import App from "./App.tsx";
5 | import "./index.css";
6 |
7 | ReactDOM.createRoot(document.getElementById("root")!).render(
8 |
9 |
10 |
11 |
12 |
13 | );
14 |
--------------------------------------------------------------------------------
/src/models/interface.ts:
--------------------------------------------------------------------------------
1 | import { Models } from "appwrite";
2 |
3 | export interface IPayload {
4 | title: string;
5 | description: string;
6 | due_date: Date;
7 | priority?: string;
8 | done?: boolean;
9 | }
10 |
11 | export interface ITask extends Models.Document {
12 | title: string;
13 | description: string;
14 | due_date: Date;
15 | priority?: string;
16 | done: boolean;
17 | }
18 |
--------------------------------------------------------------------------------
/src/routes/Index.tsx:
--------------------------------------------------------------------------------
1 | //
2 |
3 | import { useEffect, useState } from "react";
4 | import AddTask from "../components/AddTask";
5 | import { useLocation, useNavigate } from "react-router-dom";
6 | import { ITask } from "../models/interface";
7 |
8 | const Index = () => {
9 | const location = useLocation();
10 | const navigate = useNavigate();
11 |
12 | const taskFromState: ITask = location.state?.task;
13 |
14 | const [taskToEdit] = useState(taskFromState ?? null);
15 |
16 | useEffect(() => {
17 | if (taskFromState) {
18 | navigate(location.pathname, {});
19 | }
20 | }, [taskFromState, location.pathname, navigate]);
21 |
22 | return (
23 |
24 |
25 |
26 | AI-enhanced, Voice-enabled, Searchable Task Manager
27 |
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | export default Index;
35 |
--------------------------------------------------------------------------------
/src/routes/Task.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { ITask } from "../models/interface";
3 | import TaskItem from "../components/TaskItem";
4 | import { getTasks } from "../utils/shared";
5 | import Dialog from "../components/Dialog";
6 | import Button from "../components/Button";
7 | import Search from "../components/Search";
8 | import { PlusIcon } from "@heroicons/react/24/solid";
9 | import { useNavigate } from "react-router-dom";
10 | import Select from "../components/Select";
11 | import { sortByDueDate } from "../utils/db";
12 |
13 | const Task = () => {
14 | const [tasks, setTasks] = useState([]);
15 | const [tasksError, setTasksError] = useState("");
16 | const [isViewTask, setIsViewTask] = useState(false);
17 | const [selectedTask, setSelectedTask] = useState();
18 |
19 | const navigate = useNavigate();
20 |
21 | const sortByPriority = (tasksList: ITask[], isAsc: boolean): ITask[] => {
22 | const priorityOrder: { [key: string]: number } = {
23 | low: 1,
24 | medium: 2,
25 | high: 3,
26 | };
27 |
28 | return [...tasksList].sort((a, b) => {
29 | const priorityA = priorityOrder[a.priority!.toLowerCase()];
30 | const priorityB = priorityOrder[b.priority!.toLowerCase()];
31 | return isAsc ? priorityA - priorityB : priorityB - priorityA;
32 | });
33 | };
34 |
35 | const handleSelectChange = async (
36 | e: React.ChangeEvent
37 | ) => {
38 | const selectedOption = e.target.value;
39 | const doneTasks = tasks.filter((task) => task.done);
40 |
41 | switch (selectedOption) {
42 | case "priority - (low - high)":
43 | case "priority - (high - low)": {
44 | const isAsc = selectedOption === "priority - (low - high)";
45 | const sortedTasks = sortByPriority(tasks, isAsc);
46 | setTasks([...doneTasks, ...sortedTasks.filter((task) => !task.done)]);
47 | break;
48 | }
49 | case "due date - (earliest - latest)":
50 | case "due date - (latest - earliest)": {
51 | const isEarliestToLatest =
52 | selectedOption === "due date - (earliest - latest)";
53 | const dueDateResult = await sortByDueDate(isEarliestToLatest);
54 | const sortedTasks = dueDateResult.documents as ITask[];
55 | setTasks([...doneTasks, ...sortedTasks.filter((task) => !task.done)]);
56 | break;
57 | }
58 | default:
59 | break;
60 | }
61 | };
62 |
63 | const selectArray = [
64 | "priority - (low - high)",
65 | "priority - (high - low)",
66 | "due date - (earliest - latest)",
67 | "due date - (latest - earliest)",
68 | ];
69 |
70 | const handleViewTask = (activeTask: ITask) => {
71 | console.log("ACTIVE TASK" + activeTask);
72 | setIsViewTask(true);
73 | setSelectedTask(activeTask);
74 | };
75 |
76 | useEffect(() => {
77 | getTasks()
78 | .then((res) => {
79 | setTasks(res.reverse());
80 | })
81 | .catch((err) => {
82 | console.error(err);
83 | setTasksError("Error fetching tasks, please try again");
84 | });
85 | }, []);
86 | return (
87 |
88 |
89 | {isViewTask && selectedTask && (
90 |
97 | )}
98 |
99 | Your Tasks
100 |
101 |
102 |
103 |
110 |
111 | {tasksError ? (
112 | {tasksError}
113 | ) : (
114 |
115 |
116 |
Pending Tasks
117 |
118 | Sort Tasks by:
119 |
124 |
125 |
126 | {tasks
127 | .filter((task) => !task.done)
128 | .map((task) => (
129 | handleViewTask(task)}
134 | isViewTask={isViewTask}
135 | />
136 | ))}
137 |
138 |
139 |
140 |
Completed Tasks
141 |
142 | {tasks
143 | .filter((task) => task.done)
144 | .map((task) => (
145 | handleViewTask(task)}
150 | isViewTask={isViewTask}
151 | />
152 | ))}
153 |
154 |
155 |
156 | )}
157 |
158 |
159 | );
160 | };
161 |
162 | export default Task;
163 |
--------------------------------------------------------------------------------
/src/txt:
--------------------------------------------------------------------------------
1 | **Text:**
2 |
3 | - text-red-500 --text-error
4 | - text-white --text-light
5 | - text-green-600 --text-ok
6 |
7 | **Border:**
8 |
9 | - border-red-600 --border-error
10 | - border-slate-800 --border-input
11 | - border-gray-400 --border-container
12 |
13 | **Focus Ring:**
14 |
15 |
19 |
20 | **Background:**
21 |
22 | - bg-gray-200 --btn-bg-light
23 | - bg-pink-700 --btn-bg-primary
24 | - bg-green-200 --btn-bg-light-ok
25 | - bg-yellow-400 --low-priority
26 | - bg-orange-400 --medium-priority
27 | - bg-red-400 --high-priority
28 | - bg-green-400 --btn-bg-ok
29 |
30 | **Hover Effects:**
31 |
32 | - hover:bg-pink-800 --btn-bg-primary-hover
33 | - hover:shadow-lg
34 | - hover:text-red-500
35 |
36 | **Transition:**
37 |
38 | - transition duration-300 ease-in-out
39 |
--------------------------------------------------------------------------------
/src/utils/ai.ts:
--------------------------------------------------------------------------------
1 | import { HfInference } from "@huggingface/inference";
2 |
3 | // Create a new HuggingFace Inference instance
4 | const Hf = new HfInference(import.meta.env.VITE_HUGGINGFACE_KEY);
5 |
6 | // IMPORTANT! Set the runtime to edge
7 | export const runtime = "edge";
8 |
9 | export const callAI = async (prompt: string) => {
10 | const response = Hf.textGeneration({
11 | model: "mistralai/Mistral-7B-Instruct-v0.3",
12 | inputs: `<|prompter|>${prompt}<|endoftext|><|assistant|>`,
13 | parameters: {
14 | max_new_tokens: 200,
15 | // @ts-ignore (this is a valid parameter specifically in OpenAssistant models)
16 | typical_p: 0.2,
17 | repetition_penalty: 1,
18 | truncate: 1000,
19 | return_full_text: false,
20 | },
21 | });
22 |
23 | return response;
24 | };
25 |
--------------------------------------------------------------------------------
/src/utils/appwrite.ts:
--------------------------------------------------------------------------------
1 | import { Client, Databases } from "appwrite";
2 |
3 | export const client = new Client();
4 |
5 | client
6 | .setEndpoint(import.meta.env.VITE_APPWRITE_URL)
7 | .setProject(import.meta.env.VITE_APPWRITE_PROJ_ID);
8 |
9 | export const databases = new Databases(client);
10 |
11 | export { ID } from "appwrite";
12 |
--------------------------------------------------------------------------------
/src/utils/db.ts:
--------------------------------------------------------------------------------
1 | import { ID, databases } from "./appwrite";
2 | import { IPayload } from "../models/interface";
3 | import { Query } from "appwrite";
4 |
5 | const dbID: string = import.meta.env.VITE_APPWRITE_DB_ID;
6 | const collectionID: string = import.meta.env.VITE_APPWRITE_COLLECTION_ID;
7 |
8 | const createDocument = async (payload: IPayload) => {
9 | const res = await databases.createDocument(dbID, collectionID, ID.unique(), {
10 | ...payload,
11 | });
12 |
13 | return res;
14 | };
15 |
16 | const readDocuments = async () => {
17 | const res = await databases.listDocuments(dbID, collectionID);
18 |
19 | return res;
20 | };
21 |
22 | const updateDocument = async (payload: IPayload, id: string) => {
23 | const res = await databases.updateDocument(dbID, collectionID, id, {
24 | ...payload,
25 | });
26 |
27 | return res;
28 | };
29 | const deleteDocument = async (id: string) => {
30 | const res = await databases.deleteDocument(dbID, collectionID, id);
31 |
32 | return res;
33 | };
34 |
35 | const searchTasks = async (searchTerm: string) => {
36 | const resTitle = await databases.listDocuments(dbID, collectionID, [
37 | Query.search("title", searchTerm),
38 | ]);
39 | const resDesc = await databases.listDocuments(dbID, collectionID, [
40 | Query.search("description", searchTerm),
41 | ]);
42 |
43 | const res = [...resTitle.documents, ...resDesc.documents];
44 |
45 | // remove duplicate tasks
46 | const uniqueRes = res.filter(
47 | (task, index, self) => index === self.findIndex((t) => t.$id === task.$id)
48 | );
49 |
50 | return uniqueRes;
51 | };
52 | const sortByDueDate = async (isEarliestToLatest: boolean) => {
53 | const orderQuery = isEarliestToLatest
54 | ? Query.orderAsc("due_date")
55 | : Query.orderDesc("due_date");
56 | const res = await databases.listDocuments(dbID, collectionID, [orderQuery]);
57 | return res;
58 | };
59 |
60 | export {
61 | createDocument,
62 | readDocuments,
63 | updateDocument,
64 | deleteDocument,
65 | searchTasks,
66 | sortByDueDate,
67 | };
68 |
--------------------------------------------------------------------------------
/src/utils/shared.ts:
--------------------------------------------------------------------------------
1 | import { readDocuments } from "./db";
2 | import { ITask } from "../models/interface";
3 |
4 | export const getTasks = async () => {
5 | const { documents } = await readDocuments();
6 |
7 | return documents as ITask[];
8 | };
9 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
4 | darkMode: "selector",
5 | theme: {
6 | extend: {
7 | textColor: {
8 | error: "var(--text-error)",
9 | ok: "var(--text-ok)",
10 | main: "var(--text-main)",
11 | iconColor: "var(--btn-icon-main)",
12 | },
13 | backgroundColor: {
14 | base: "var(--base-bg)",
15 | primary: "var(--btn-bg-primary)",
16 | primaryHover: "var(--btn-bg-primary-hover)",
17 | ok: "var(--btn-bg-ok)",
18 | lightOk: "var(--btn-bg-light-ok)",
19 | light: "var(--btn-bg-light)",
20 | lowPriority: "var(--low-priority)",
21 | mediumPriority: "var(--medium-priority)",
22 | highPriority: "var(--high-priority)",
23 | },
24 | borderColor: {
25 | container: "var(--border-container)",
26 | input: "var(--border-input)",
27 | error: "var(--border-error)",
28 | },
29 | },
30 | },
31 | plugins: [],
32 | };
33 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": ["src"],
24 | "references": [{ "path": "./tsconfig.node.json" }]
25 | }
26 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
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 |
--------------------------------------------------------------------------------