diff --git a/bode/bode/resources/tasks/api.py b/bode/bode/resources/tasks/api.py index 02b7b5f..bd376fd 100644 --- a/bode/bode/resources/tasks/api.py +++ b/bode/bode/resources/tasks/api.py @@ -15,7 +15,7 @@ class Tasks(MethodView): @blueprint.response(200, TaskSchema(many=True)) def get(self): - return Task.query.all() + return Task.query.order_by(Task.is_done, Task.due_date).all() @blueprint.arguments(TaskInputSchema) @blueprint.response(201, TaskSchema) diff --git a/cabra/index.html b/cabra/index.html index 20c5a48..913a086 100644 --- a/cabra/index.html +++ b/cabra/index.html @@ -5,6 +5,9 @@ Cabra +
diff --git a/cabra/src/pages/Home.tsx b/cabra/src/pages/Home.tsx index ee6e84d..cb12d97 100644 --- a/cabra/src/pages/Home.tsx +++ b/cabra/src/pages/Home.tsx @@ -1,18 +1,25 @@ import "twin.macro"; -import CreateTaskForm from "./components/CreateTaskForm"; import Layout from "./components/Layout"; -import RandomGoat from "./components/RandomGoat"; +import { Link } from "react-router-dom"; +import NavigationButton from "./components/NavigationButton"; +import { PlusIcon } from "@heroicons/react/solid"; import TasksList from "./components/TasksList"; +import { routeHelpers } from "../routes"; export default function HomePage() { return ( -
- +
+
+ + + Add new task + + +
- ); } diff --git a/cabra/src/pages/TaskCreate.tsx b/cabra/src/pages/TaskCreate.tsx new file mode 100644 index 0000000..a2f3442 --- /dev/null +++ b/cabra/src/pages/TaskCreate.tsx @@ -0,0 +1,28 @@ +import "twin.macro"; + +import { ArrowLeftIcon } from "@heroicons/react/solid"; +import CreateTaskForm from "./components/CreateTaskForm"; +import Layout from "./components/Layout"; +import NavigationButton from "./components/NavigationButton"; +import { useNavigate } from "react-router-dom"; + +export default function TaskCreate() { + const navigate = useNavigate(); + return ( + +
+
+

Create new task

+ navigate(-1)} + > + + Go Back + +
+ +
+
+ ); +} diff --git a/cabra/src/pages/TaskDetails.tsx b/cabra/src/pages/TaskDetails.tsx index c5dd260..f191353 100644 --- a/cabra/src/pages/TaskDetails.tsx +++ b/cabra/src/pages/TaskDetails.tsx @@ -14,7 +14,7 @@ export default function TaskDetailsPage() { return ( navigate(-1)} > Go Back diff --git a/cabra/src/pages/TaskEdit.tsx b/cabra/src/pages/TaskEdit.tsx index 2fa9bdb..0fc7f72 100644 --- a/cabra/src/pages/TaskEdit.tsx +++ b/cabra/src/pages/TaskEdit.tsx @@ -38,11 +38,11 @@ export default function TaskEditPage() { if (isLoading || !data) return Loading; return ( -
+

Edit task

navigate(-1)} > @@ -52,7 +52,7 @@ export default function TaskEditPage() {
- +
diff --git a/cabra/src/pages/components/AddDependenceButton.tsx b/cabra/src/pages/components/AddDependenceButton.tsx deleted file mode 100644 index 4de7c56..0000000 --- a/cabra/src/pages/components/AddDependenceButton.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import tw, { styled } from "twin.macro"; - -const AddDependenceButton = styled.button( - tw`rounded-lg flex flex-row items-start h-5 py-1 px-4 w-16 bg-tertiary font-bold text-lg color[#787DAB]` -); - -export default AddDependenceButton; diff --git a/cabra/src/pages/components/CheckBox.tsx b/cabra/src/pages/components/CheckBox.tsx index e881fc9..24ced50 100644 --- a/cabra/src/pages/components/CheckBox.tsx +++ b/cabra/src/pages/components/CheckBox.tsx @@ -1,15 +1,57 @@ -import tw, { styled } from "twin.macro"; +import tw, { TwStyle, styled } from "twin.macro"; +import { CheckCircleIcon } from "@heroicons/react/outline"; import { InputHTMLAttributes } from "react"; -const StyledCheckboxInput = styled.input( - tw`flex items-center gap-2 p-2 rounded checked:bg-blue-500 checked:border-blue-500` -); - -type Props = Omit, "type"> & { +type Props = Omit< + InputHTMLAttributes, + "type" | "id" | "size" +> & { className?: string; + id: string; + size?: Size; +}; +type Size = "sm" | "base" | "lg"; + +const StyledInput = styled.input(tw`absolute h-0 w-0 opacity-0`); + +const labelSizes: Record = { + sm: tw`w-7 h-7`, + base: tw`w-9 h-9`, + lg: tw`w-11 h-11`, +}; + +const iconSizes: Record = { + sm: { height: 20, width: 20 }, + base: { height: 24, width: 24 }, + lg: { height: 28, width: 28 }, }; -export default function Checkbox(props: Props) { - return ; +const Label = styled.label<{ size: Size }>` + ${tw`select-none`} + + .checkmark { + ${tw`place-items-center rounded grid cursor-pointer`} + ${tw`bg-slate-300 text-slate-400 transition-all shadow-xl hover:opacity-60`} + ${({ size }) => labelSizes[size]} + } + + input:checked ~ .checkmark { + ${tw`bg-success text-green-900`} + } + + input:focus ~ .checkmark { + ${tw`ring-2`} + } +`; + +export default function Checkbox({ id, size = "base", ...props }: Props) { + return ( + + ); } diff --git a/cabra/src/pages/components/CreateTaskForm.tsx b/cabra/src/pages/components/CreateTaskForm.tsx index 9507a2a..7386c43 100644 --- a/cabra/src/pages/components/CreateTaskForm.tsx +++ b/cabra/src/pages/components/CreateTaskForm.tsx @@ -3,6 +3,8 @@ import { useMutation, useQueryClient } from "react-query"; import { ITask } from "../../types/task"; import TaskForm from "./TaskForm"; +import { routeHelpers } from "../../routes"; +import { useNavigate } from "react-router-dom"; const emptyTask: Omit = { description: "", @@ -13,9 +15,13 @@ const emptyTask: Omit = { }; export default function CreateTaskForm() { + const navigate = useNavigate(); const client = useQueryClient(); const addTask = useMutation(createTask, { - onSuccess: () => client.invalidateQueries(getTasks.cacheKey), + onSuccess: () => { + client.invalidateQueries(getTasks.cacheKey); + navigate(routeHelpers.tasks); + }, }); return ; diff --git a/cabra/src/pages/components/MiniTaskDelete.tsx b/cabra/src/pages/components/MiniTaskDelete.tsx index 5b12d6b..a98cf25 100644 --- a/cabra/src/pages/components/MiniTaskDelete.tsx +++ b/cabra/src/pages/components/MiniTaskDelete.tsx @@ -15,7 +15,7 @@ export default function MiniTaskDelete({ taskId, }: Props) { return ( -
+

{title} {

{subtask.title}

- +

{errorMessage && (

 {errorMessage}

diff --git a/cabra/src/pages/components/SubtasksListEdit.tsx b/cabra/src/pages/components/SubtasksListEdit.tsx index 199cdfe..86d266f 100644 --- a/cabra/src/pages/components/SubtasksListEdit.tsx +++ b/cabra/src/pages/components/SubtasksListEdit.tsx @@ -4,13 +4,19 @@ import { createTask, deleteTask, getTask, getTasks } from "../../api/tasks"; import tw, { styled } from "twin.macro"; import { useMutation, useQuery, useQueryClient } from "react-query"; -import AddDependenceButton from "./AddDependenceButton"; import { ITask } from "../../types/task"; import { ITaskRelation } from "../../types/taskRelation"; import MiniTaskDelete from "./MiniTaskDelete"; +import { PlusIcon } from "@heroicons/react/solid"; -const Container = styled.div(tw`text-gray-50 w-full space-y-4`); -const fieldStyles = tw`w-full px-4 py-2 rounded-xl text-blue-800 placeholder:text-blue-800/60 font-size[small]`; +const Container = styled.div(tw`text-gray-50 w-full space-y-2`); +const AddSubtaskButton = styled.button( + tw`bg-secondary p-2 text-white font-semibold`, + tw`rounded shadow-2xl flex gap-2 transition-transform transform hover:scale-105` +); +const SubtaskInput = styled.input( + tw`form-input w-full px-4 py-2 rounded-lg shadow-2xl bg-tertiary text-black placeholder:text-primary/60` +); const emptyTask: Omit = { description: "", @@ -77,33 +83,31 @@ export default function SubtasksListEdit({ parentId }: Props) { return (
- - {subtasks.map((relatedTask) => ( - - ))} -
-
-

- setVal(event.target.value)} - /> -

-

- +add -

+
+ setVal(event.target.value)} + placeholder="Subtask name" + required + type="text" + value={val} + /> + + Add +
+ + {subtasks.map((relatedTask) => ( + + ))} +
); diff --git a/cabra/src/pages/components/Task.tsx b/cabra/src/pages/components/Task.tsx deleted file mode 100644 index 612b8c7..0000000 --- a/cabra/src/pages/components/Task.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import "twin.macro"; - -import { getTask, getTasks, updateTask } from "../../api/tasks"; -import { useMutation, useQueryClient } from "react-query"; - -import CheckBox from "./CheckBox"; -import { ITask } from "../../types/task"; -import { Link } from "react-router-dom"; -import NavigationButton from "./NavigationButton"; -import { formatDateTime } from "../../utils/dates"; -import { routeHelpers } from "../../routes"; -import { useState } from "react"; - -interface Props { - task: ITask; - detailsLink: boolean; -} - -export default function Task({ task, detailsLink }: Props) { - const [errorMessage, setErrorMessage] = useState(""); - const client = useQueryClient(); - const editTask = useMutation((task: ITask) => updateTask(task.id, task), { - onSuccess: () => { - client.invalidateQueries(getTasks.cacheKey); - client.invalidateQueries(getTask.cacheKey(task.id)); - }, - }); - const handleIsDoneChange = async () => { - try { - const updatedTask = { - ...task, - isDone: !task.isDone, - }; - await editTask.mutateAsync(updatedTask); - } catch (error) { - setErrorMessage( - "Something went wrong :C, It's not possible to uncheck the task." - ); - } - }; - - return ( -
-

- {task.title} - {} -

-

- {formatDateTime(task.dueDate)} - {detailsLink && ( - - Details - - )} -

- {errorMessage && ( -

 {errorMessage}

- )} -
- ); -} diff --git a/cabra/src/pages/components/TaskDetailsCard.tsx b/cabra/src/pages/components/TaskDetailsCard.tsx index 08a7521..0fe8475 100644 --- a/cabra/src/pages/components/TaskDetailsCard.tsx +++ b/cabra/src/pages/components/TaskDetailsCard.tsx @@ -25,10 +25,6 @@ const Ctas = styled(CardField)` ${tw`flex place-content-around`} `; -const StatusCheckbox = styled(CheckBox)` - ${tw`h-8 w-8`} -`; - interface Props { id: string; } @@ -50,20 +46,21 @@ export default function TaskDetails({ id }: Props) { {task?.dueDate ? formatDateTime(task.dueDate) : ""} - - + removeTask()} > - + diff --git a/cabra/src/pages/components/TaskForm.tsx b/cabra/src/pages/components/TaskForm.tsx index 4670f2c..fa40c56 100644 --- a/cabra/src/pages/components/TaskForm.tsx +++ b/cabra/src/pages/components/TaskForm.tsx @@ -28,24 +28,24 @@ const schema = yup.object({ dueDate: yup.date().nullable(), }); -const fieldStyles = tw`w-full px-4 py-2 rounded shadow-2xl text-blue-800 placeholder:text-blue-800/60`; +const fieldStyles = tw`w-full px-4 py-2 rounded-lg shadow-2xl bg-tertiary text-black placeholder:text-primary/60`; const Form = styled.form` - ${tw`flex flex-col space-y-4 w-full`} + ${tw`flex flex-col w-full`} .react-datepicker-wrapper { ${tw` text-blue-800 placeholder:text-blue-800/60`} input[type="text"] { max-width: 100%; - ${tw`rounded shadow-2xl`} + ${tw`bg-tertiary rounded-lg shadow-2xl text-black`} } } `; -const Label = styled.label(tw`text-gray-50`); +const Label = styled.label(tw`text-gray-50 font-bold`); const SubmitButton = styled.button( - tw`bg-gradient-to-r from-green-400 to-cyan-500 py-2 text-white font-semibold`, - tw`rounded shadow-2xl transition-transform transform hover:scale-105 disabled:opacity-50` + tw`bg-green-500 mt-4 py-2 text-white font-semibold`, + tw`rounded shadow-2xl transition-transform transform hover:scale-[102%] disabled:opacity-50` ); export default function TaskForm({ task, onSubmit }: Props) { @@ -83,21 +83,32 @@ export default function TaskForm({ task, onSubmit }: Props) { return (
-
-
- +
+
+
-
- +
+ + +
+
+
-
-
- - -
+
Submit! -
 {errors.title?.message}
+
 {errors.title?.message}
); } diff --git a/cabra/src/pages/components/TaskListItem.tsx b/cabra/src/pages/components/TaskListItem.tsx new file mode 100644 index 0000000..e82dae6 --- /dev/null +++ b/cabra/src/pages/components/TaskListItem.tsx @@ -0,0 +1,85 @@ +import { getTask, updateTask } from "../../api/tasks"; +import tw, { styled } from "twin.macro"; +import { useMutation, useQueryClient } from "react-query"; + +import { ArrowRightIcon } from "@heroicons/react/solid"; +import CheckBox from "./CheckBox"; +import { ITask } from "../../types/task"; +import { Link } from "react-router-dom"; +import NavigationButton from "./NavigationButton"; +import { formatDateTime } from "../../utils/dates"; +import { routeHelpers } from "../../routes"; +import { useState } from "react"; + +interface Props { + task: ITask; +} + +const Card = styled.div(tw`p-2 w-full bg-secondary rounded-lg`); +const Column = styled.div(tw`flex flex-col gap-2`); +const TagChip = styled.div(tw`rounded-full px-2 bg-tertiary text-secondary`); + +export default function TaskListItem({ task }: Props) { + const [errorMessage, setErrorMessage] = useState(""); + const [isDone, setIsDone] = useState(() => task.isDone); + const client = useQueryClient(); + const editTask = useMutation((task: ITask) => updateTask(task.id, task), { + onSuccess: () => { + client.invalidateQueries(getTask.cacheKey(task.id)); + }, + }); + const handleIsDoneChange = async () => { + try { + const updatedTask = { + ...task, + isDone: !task.isDone, + }; + setIsDone(!isDone); + await editTask.mutateAsync(updatedTask); + } catch (error) { + setIsDone(isDone); + setErrorMessage( + "Something went wrong :C, It's not possible to uncheck the task." + ); + } + }; + + return ( +
+ + {task.title} + + Tags: + {task.tags.length > 0 + ? task.tags.map((tag) => {tag.name}) + : " No tags found"} + + + + {task.dueDate && ( + + {formatDateTime(task.dueDate)} + + )} +
+ + + + + More + + + +
+
+ {errorMessage && ( +

 {errorMessage}

+ )} +
+ ); +} diff --git a/cabra/src/pages/components/TasksList.tsx b/cabra/src/pages/components/TasksList.tsx index f3f8738..81e5b5b 100644 --- a/cabra/src/pages/components/TasksList.tsx +++ b/cabra/src/pages/components/TasksList.tsx @@ -1,7 +1,6 @@ import tw, { styled } from "twin.macro"; import TasksListPaginator from "./TasksListsPaginator"; -import { compareAsc } from "date-fns"; import { getTasks } from "../../api/tasks"; import { useQuery } from "react-query"; @@ -13,23 +12,16 @@ export default function TasksList() { if (isLoading) return Loading; if (error || !data?.data) return Oops; - const tasks = data.data.slice().reverse(); - - const tasksSortedByDueDate = tasks.sort((a, b) => - a.dueDate && b.dueDate - ? compareAsc(new Date(a.dueDate), new Date(b.dueDate)) - : -1 - ); + const tasks = data.data; + const hasNoTasks = !tasks || tasks.length == 0; return ( - <> - {!tasks || - (tasks.length == 0 && ( -

No tasks in your agenda. Go ahead, add one!

- ))} - {tasks && tasks.length > 0 && ( - + + {hasNoTasks ? ( +

No tasks in your agenda. Go ahead, add one!

+ ) : ( + )} - +
); } diff --git a/cabra/src/pages/components/TasksListsPaginator.tsx b/cabra/src/pages/components/TasksListsPaginator.tsx index 3cc23d3..a696674 100644 --- a/cabra/src/pages/components/TasksListsPaginator.tsx +++ b/cabra/src/pages/components/TasksListsPaginator.tsx @@ -4,7 +4,7 @@ import { useEffect, useState } from "react"; import { ITask } from "../../types/task"; import ReactPaginate from "react-paginate"; import Select from "react-select"; -import Task from "./Task"; +import TaskListItem from "./TaskListItem"; const Container = styled.div(tw`text-gray-50 w-full space-y-4`); @@ -17,8 +17,6 @@ interface PaginationOption { } const paginationOptions: PaginationOption[] = [ - { value: 1, label: "1" }, - { value: 2, label: "2" }, { value: 5, label: "5" }, { value: 10, label: "10" }, { value: 20, label: "20" }, @@ -48,14 +46,13 @@ export default function TasksListsPaginator({ items }: Props) { <>
- {paginatedItems && - paginatedItems.map((item: ITask) => ( - - ))} + {paginatedItems?.map((item: ITask) => ( + + ))}
-
+
Items per page: {itemsPerPage}