Skip to content

Commit

Permalink
feat: reorganize files and add comments
Browse files Browse the repository at this point in the history
  • Loading branch information
aannirajpatel committed Jan 6, 2024
1 parent 7f5203c commit 34123d0
Show file tree
Hide file tree
Showing 12 changed files with 2,007 additions and 1,712 deletions.
32 changes: 32 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
]
},
"devDependencies": {
"@types/uuid": "^9.0.7",
"typescript": "^4.9.5"
}
}
35 changes: 35 additions & 0 deletions src/AddCategoryDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Check } from '@mui/icons-material';
import { Dialog, Input, Paper } from '@mui/material';
import Button from '@mui/material/Button';
import React from 'react';

export function AddCategoryDialog({ addCategoryDialogOpen, setAddCategoryDialogOpen, handleAddCategory }: { addCategoryDialogOpen: boolean; setAddCategoryDialogOpen: React.Dispatch<React.SetStateAction<boolean>>; handleAddCategory: (category: string) => void; }) {
const [category, setCategory] = React.useState('');
return <Dialog open={addCategoryDialogOpen} onClose={() => setAddCategoryDialogOpen(false)}>
<Paper style={{ minWidth: 300, paddingInline: 16, paddingBottom: 16 }}>
<h3>Add a new category</h3>
<div style={{ display: 'flex', gap: 8 }}>
<Input placeholder="Type a category name" fullWidth
onKeyDown={
(event) => {
if (event.key === 'Enter') {
setCategory('');
handleAddCategory(category);
}
}
}
onChange={(event) => {setCategory(event.target.value); }}
value={category} />
<Button variant="contained"
onClick={
() => {
if(category === '') return;
handleAddCategory(category);
setAddCategoryDialogOpen(false);
}} startIcon={<Check />}>
Done
</Button>
</div>
</Paper>
</Dialog>;
}
214 changes: 74 additions & 140 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,84 +1,113 @@
import { Add, AutoFixHigh, Check, Delete, Edit, ExpandMore } from '@mui/icons-material';
import AppRegistrationIcon from '@mui/icons-material/AppRegistration';
import { Accordion, AccordionActions, AccordionDetails, AccordionSummary, CssBaseline, Dialog, Fab, Input, List, ListItemButton, ListItemText, Paper, Tooltip } from '@mui/material';
import { Add, Delete, ExpandMore } from '@mui/icons-material';
import { Accordion, AccordionActions, AccordionDetails, AccordionSummary, CssBaseline, Input } from '@mui/material';
import Button from '@mui/material/Button';
import React from 'react';
import { v4 } from 'uuid';
import './App.css';
import { createEmbedder, getCosineSimilarity } from './MediaPipe.ts';
import { useQuery } from '@tanstack/react-query';
import { MagicBtn } from './MagicBtn.tsx';
import { AddCategoryDialog } from './AddCategoryDialog.tsx';
import { TaskContainer } from './TaskContainer.tsx';

interface Task {
export interface Task {
id: string;
content: string;
categoryId: number;
}

function App() {
const { isLoading, isError, error } = useQuery({queryKey: ['embedder'], queryFn: createEmbedder});
// This query is used to load the MediaPipe embedder. It is a one-time operation.
const { isLoading, isError, error } = useQuery({
queryKey: ['loadMediaPipeEmbedder'],
queryFn: createEmbedder
});

// Declare state variables used to store the tasks and categories.
const [todos, setTodos] = React.useState<Task[]>([{ id: v4(), content: 'Example task', categoryId: 0 }]);
const [categories, setCategories] = React.useState<string[]>(["Unorganized"]);
// A 2D array of tasks, where the first dimension is the category index.
// tasksByCategory[0] is always the list of tasks in the first category, "Unorganized"
const tasksByCategory: Task[][] = categories.map((_category, categoryIndex) => todos.filter((todo) => todo.categoryId === categoryIndex));

const handleDeleteTask = (id: string) => {
setTodos(todos.filter((todo, index) => todo.id !== id));
};
const [todo, setTodo] = React.useState<Task>({ id: v4(), content: '', categoryId: 0 }); // holds the current task being created
const [addCategoryDialogOpen, setAddCategoryDialogOpen] = React.useState(false);

// These are functions that are used to update the tasks and categories.
const handleAddTask = () => {
if (todo.content === '') return;
setTodos([...todos, todo]);
setTodo({ id: v4(), content: '', categoryId: 0 });
}

const handleUpdateTask = (id: string, content: string) => {
setTodos(todos.map((todo, index) => todo.id === id ? { ...todo, content: content } : todo));
setTodos(todos.map((todo) => todo.id === id ? { ...todo, content: content } : todo));
};

const handleUpdateCategory = (id: string, categoryId: number) => {
setTodos(todos.map((todo, index) => todo.id === id ? { ...todo, categoryId: categoryId } : todo));
const handleDeleteTask = (id: string) => {
setTodos(todos.filter((todo) => todo.id !== id));
};

const handleAddCategory = (category: string) => {
// if the category already exists, do not add it
for (let i = 0; i < categories.length; i++) {
if (categories[i] === category) return;
}
setCategories([...categories, category]);
}

const handleUpdateCategory = (id: string, categoryId: number) => {
setTodos(todos.map((todo, index) => todo.id === id ? { ...todo, categoryId: categoryId } : todo));
};

const handleDeleteCategory = (id: number) => {
if (id === 0) return; // cannot remove unorganized category - it is the default category
setCategories(categories.filter((_category, index) => index !== id));
}

/**
* Utility function that is used to reorganize the tasks based on their similarity to the categories.
* This function is called when the user clicks on the magic button.
* @returns void
*/
const handleSmartReorganize = () => {
if(isError) {console.error("MediaPipe Error: ",error);}
if(isLoading || isError) return;
const newTodos:Task[] = [];
todos.forEach((todo) => {
const maxMatch = { categoryId: 0, similarity: 0 };
if (isError) { console.error("MediaPipe Error: ", error); }
if (isLoading || isError) return;
const newTodos: Task[] = [];
todos.forEach((todo) => { // find the best matching category for each task
const bestMatch = { categoryId: 0, similarity: 0 };
categories.forEach((category, index) => {
const similarity = getCosineSimilarity(todo.content, category);
if (similarity > maxMatch.similarity && index !== 0) {
maxMatch.categoryId = index;
maxMatch.similarity = similarity;
if (similarity > bestMatch.similarity && index !== 0) {
bestMatch.categoryId = index;
bestMatch.similarity = similarity;
}
});
// console.log("MAX:",maxMatch.categoryId, categories[maxMatch.categoryId], maxMatch.similarity, todo.content);
newTodos.push({ ...todo, categoryId: maxMatch.categoryId });
newTodos.push({ ...todo, categoryId: bestMatch.categoryId });
});
setTodos(newTodos);
}

const tasksByCategory: Task[][] = categories.map((_category, categoryIndex) => todos.filter((todo) => todo.categoryId === categoryIndex));

const [task, setTask] = React.useState<Task>({ id: v4(), content: '', categoryId: 0 });

const handleAddTask = () => {
if (task.content === '') return;
setTodos([...todos, task]);
setTask({ id: v4(), content: '', categoryId: 0 });
}

const [addCategoryDialogOpen, setAddCategoryDialogOpen] = React.useState(false);

return (
<React.Fragment>
<CssBaseline />
<div style={{ paddingInline: 32, paddingBottom: 32, display: 'flex', flexDirection: 'column' }}>
<h1>SmarTodos!</h1>
<div style={{ display: 'flex', gap: 16, flexDirection: 'column' }}>
<div style={{ display: 'flex', gap: 16 }}>
<Input placeholder="Type a todo" fullWidth onChange={(event) => { setTask(t => ({ ...t, content: event.target.value })); }} value={task.content} />
<Input // input field for creating a new task
placeholder="Type a todo"
fullWidth
onKeyDown={(event) => {
if (event.key === 'Enter') {
handleAddTask();
}
}}
onChange={(event) => {
setTodo((t) => ({ ...t, content: event.target.value }));
}}
value={todo.content}
/>
<Button variant="contained" onClick={handleAddTask}>Create task</Button>
</div>
<div>
Expand All @@ -87,18 +116,21 @@ function App() {
<div style={{ display: 'flex', justifyContent: 'center' }}>
<div style={{ display: 'flex', flexDirection: 'column', width: '50vw', gap: 16 }}>
{
tasksByCategory.map((tasks, index) => {
return <Accordion>
tasksByCategory.map((tasks, index) => { // for each category, show a list of tasks in an accordion
return <Accordion key={`category-accordion-${index}`}>
<AccordionSummary expandIcon={<ExpandMore />}>
<h3>{categories[index]}</h3>
</AccordionSummary>
{index > 0 && <AccordionActions>
<Button variant="contained" color="error" onClick={() => handleDeleteCategory(index)} startIcon={<Delete />}>Delete</Button>
</AccordionActions>}
{ // for all categories except the "Unorganized" category, show a delete button
index > 0 &&
<AccordionActions>
<Button variant="contained" color="error" onClick={() => handleDeleteCategory(index)} startIcon={<Delete />}>Delete</Button>
</AccordionActions>
}
<AccordionDetails>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
{tasks.map((task) => {
return <TaskContainer task={task} handleDeleteTask={handleDeleteTask} handleUpdateTask={handleUpdateTask} categories={categories} handleUpdateCategory={handleUpdateCategory} />
{tasks.map((task) => { // for each task, render a TaskContainer
return <TaskContainer key={task.id} task={task} handleDeleteTask={handleDeleteTask} handleUpdateTask={handleUpdateTask} categories={categories} handleUpdateCategory={handleUpdateCategory} />
})}
</div>
</AccordionDetails>
Expand All @@ -115,102 +147,4 @@ function App() {
);
}

const TaskContainer = (props: { task: Task, handleDeleteTask: (id: string) => void, handleUpdateTask: (id: string, content: string) => void, categories: string[], handleUpdateCategory: (id: string, categoryId: number) => void }) => {
const [openEditDialog, setOpenEditDialog] = React.useState(false);
const [openCategoryDialog, setOpenCategoryDialog] = React.useState(false);
const [editedTaskContent, setEditedTaskContent] = React.useState(props.task.content);
return <div style={{ display: 'flex', gap: 16, background: '#eee', padding: 16, borderRadius: 8 }}>
<p>{props.task.content}</p>
<DeleteBtn {...props} />
<EditBtn setOpenEditDialog={setOpenEditDialog} />
<Dialog open={openEditDialog} onClose={() => setOpenEditDialog(false)}>
<Paper style={{ minWidth: 300, paddingInline: 16, paddingBottom: 16 }}>
<h3>Edit todo</h3>
<div style={{ display: 'flex', gap: 8 }}>
<Input placeholder="Replace this todo with a fresh one" fullWidth value={editedTaskContent} onChange={(event) => setEditedTaskContent(event.target.value)} />
<Button variant="contained" onClick={() => { props.handleUpdateTask(props.task.id, editedTaskContent); setOpenEditDialog(false) }}>Save</Button>
</div>
</Paper>
</Dialog>
<CategoryBtn setOpenCategoryDialog={setOpenCategoryDialog} />
<Dialog open={openCategoryDialog} onClose={() => setOpenCategoryDialog(false)}>
<Paper style={{ minWidth: 300, paddingInline: 16 }}>
<h3>Update the category for your task</h3>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<List>
{props.categories.map((category, index) => {
return (
<ListItemButton key={index} onClick={() => {
props.handleUpdateCategory(props.task.id, index);
setOpenCategoryDialog(false);
}}>
<ListItemText primary={category} />
</ListItemButton>
);
})}
</List>
</div>
</Paper>
</Dialog>
</div>
};

export default App;

const MagicBtn = ({onClick}: {onClick: ()=>void}) => (
<div style={{ position: 'fixed', bottom: '20px', right: '20px' }}>
<Tooltip title="Smart re-organize tasks">
<Fab color="primary" aria-label="Magic organize" variant="extended" onClick={onClick}>
<AutoFixHigh />
</Fab>
</Tooltip>
</div>
);
function AddCategoryDialog({ addCategoryDialogOpen, setAddCategoryDialogOpen, handleAddCategory }: { addCategoryDialogOpen: boolean, setAddCategoryDialogOpen: React.Dispatch<React.SetStateAction<boolean>>, handleAddCategory: (category: string) => void }) {
const [category, setCategory] = React.useState('');
return <Dialog open={addCategoryDialogOpen} onClose={() => setAddCategoryDialogOpen(false)}>
<Paper style={{ minWidth: 300, paddingInline: 16, paddingBottom: 16 }}>
<h3>Add a new category</h3>
<div style={{ display: 'flex', gap: 8 }}>
<Input placeholder="Type a category name" fullWidth onChange={(event) => { setCategory(event.target.value); }} value={category} />
<Button variant="contained" onClick={() => { handleAddCategory(category); setAddCategoryDialogOpen(false); }} startIcon={<Check />}>Done</Button>
</div>
</Paper>
</Dialog>;
}

function CategoryBtn({ setOpenCategoryDialog }: { setOpenCategoryDialog: React.Dispatch<React.SetStateAction<boolean>> }) {
return (
<Tooltip title="Update category">
<Button onClick={() => setOpenCategoryDialog(true)}>
<AppRegistrationIcon />
</Button>
</Tooltip>
);
}

function EditBtn({ setOpenEditDialog }: { setOpenEditDialog: React.Dispatch<React.SetStateAction<boolean>> }) {
return (
<Tooltip title="Edit">
<Button onClick={() => setOpenEditDialog(true)}>
<Edit />
</Button>
</Tooltip>
);
}

function DeleteBtn(props: {
task: Task;
handleDeleteTask: (id: string) => void;
handleUpdateTask: (id: string, content: string) => void;
categories: string[];
handleUpdateCategory: (id: string, categoryId: number) => void;
}) {
return (
<Tooltip title="Delete">
<Button onClick={() => { props.handleDeleteTask(props.task.id); }}>
<Delete />
</Button>
</Tooltip>
);
}
export default App;
Loading

0 comments on commit 34123d0

Please sign in to comment.