diff --git a/backend/src/main/java/org/example/app/task/logic/UcFindTaskList.java b/backend/src/main/java/org/example/app/task/logic/UcFindTaskList.java index 4cf1cd8..2cc8b0e 100644 --- a/backend/src/main/java/org/example/app/task/logic/UcFindTaskList.java +++ b/backend/src/main/java/org/example/app/task/logic/UcFindTaskList.java @@ -1,5 +1,6 @@ package org.example.app.task.logic; +import java.util.List; import java.util.Optional; import jakarta.enterprise.context.ApplicationScoped; @@ -40,6 +41,16 @@ public TaskListEto findById(Long listId) { return taskList.map(taskListEntity -> this.taskListMapper.toEto(taskListEntity)).orElse(null); } + /** + * @return all {@link TaskListEto} or {@code []} if not found. + */ + // @RolesAllowed(ApplicationAccessControlConfig.PERMISSION_FIND_TASK_LIST) + public List findAll() { + + List taskList = this.taskListRepository.findAll(); + return taskList.stream().map(taskListEntity -> this.taskListMapper.toEto(taskListEntity)).toList(); + } + /** * @param listId the {@link TaskListEntity#getId() primary key} of the {@link TaskListEntity} to find. * @return the {@link TaskListCto} for the given {@link TaskListEto#getId() primary key} or {@code null} if not found. diff --git a/backend/src/main/java/org/example/app/task/logic/UcSaveTaskItem.java b/backend/src/main/java/org/example/app/task/logic/UcSaveTaskItem.java index 92b5e5d..9b2564b 100644 --- a/backend/src/main/java/org/example/app/task/logic/UcSaveTaskItem.java +++ b/backend/src/main/java/org/example/app/task/logic/UcSaveTaskItem.java @@ -32,11 +32,11 @@ public class UcSaveTaskItem { * @return the {@link TaskItemEntity#getId() primary key} of the saved {@link TaskItemEntity}. */ // @RolesAllowed(ApplicationAccessControlConfig.PERMISSION_SAVE_TASK_ITEM) - public Long save(TaskItemEto item) { + public TaskItemEntity save(TaskItemEto item) { TaskItemEntity entity = this.taskItemMapper.toEntity(item); entity = this.taskItemRepository.save(entity); - return entity.getId(); + return entity; } } diff --git a/backend/src/main/java/org/example/app/task/logic/UcSaveTaskList.java b/backend/src/main/java/org/example/app/task/logic/UcSaveTaskList.java index 79e3e80..6bdffd2 100644 --- a/backend/src/main/java/org/example/app/task/logic/UcSaveTaskList.java +++ b/backend/src/main/java/org/example/app/task/logic/UcSaveTaskList.java @@ -28,11 +28,11 @@ public class UcSaveTaskList { * @return the {@link TaskListEntity#getId() primary key} of the saved {@link TaskListEntity}. */ // @RolesAllowed(ApplicationAccessControlConfig.PERMISSION_SAVE_TASK_LIST) - public Long save(TaskListEto list) { + public TaskListEntity save(TaskListEto list) { TaskListEntity entity = this.taskListMapper.toEntity(list); entity = this.taskListRepository.save(entity); - return entity.getId(); + return entity; } } diff --git a/backend/src/main/java/org/example/app/task/service/TaskService.java b/backend/src/main/java/org/example/app/task/service/TaskService.java index eea9ef3..ab54f12 100644 --- a/backend/src/main/java/org/example/app/task/service/TaskService.java +++ b/backend/src/main/java/org/example/app/task/service/TaskService.java @@ -22,6 +22,8 @@ import org.example.app.task.common.TaskItemEto; import org.example.app.task.common.TaskListCto; import org.example.app.task.common.TaskListEto; +import org.example.app.task.dataaccess.TaskItemEntity; +import org.example.app.task.dataaccess.TaskListEntity; import org.example.app.task.logic.UcAddRandomActivityTaskItem; import org.example.app.task.logic.UcDeleteTaskItem; import org.example.app.task.logic.UcDeleteTaskList; @@ -31,7 +33,9 @@ import org.example.app.task.logic.UcSaveTaskList; import java.net.URI; +import java.util.List; import java.util.Map; +import java.util.Objects; /** * Rest service for {@link org.example.app.task.common.TaskList}. @@ -74,11 +78,11 @@ public class TaskService { @APIResponse(responseCode = "500", description = "Server unavailable or a server-side error occurred") public Response saveTask(@Valid TaskListEto taskList) { - Long taskListId = this.ucSaveTaskList.save(taskList); - if (taskList.getId() == null || taskList.getId() != taskListId) { - return Response.created(URI.create("/task/list/" + taskListId)).build(); + TaskListEntity savedTaskList = this.ucSaveTaskList.save(taskList); + if (taskList.getId() == null || !Objects.equals(taskList.getId(), savedTaskList.getId())) { + return Response.created(URI.create("/task/list/" + savedTaskList.getId())).entity(savedTaskList.getId()).build(); } - return Response.ok().build(); + return Response.ok(savedTaskList.getVersion()).build(); } /** @@ -102,6 +106,19 @@ public TaskListEto findTaskList( return task; } + /** + * @return all {@link TaskListEto}. + */ + @GET + @Path("/lists") + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Fetch task lists", description = "Fetch all task list") + @APIResponse(responseCode = "200", description = "Task lists", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = TaskListEto[].class))) + @APIResponse(responseCode = "500", description = "Server unavailable or a server-side error occurred") + public List findTaskLists() { + return this.ucFindTaskList.findAll(); + } + /** * @param id the {@link TaskListEto#getId() primary key} of the requested {@link TaskListEto}. * @return the {@link TaskListEto} for the given {@code id}. @@ -167,10 +184,10 @@ public Response addMultipleRandomActivities(@NotBlank @Schema(required = true, e TaskListEto taskList = new TaskListEto(); taskList.setTitle(listTitle); - Long taskListId = this.ucSaveTaskList.save(taskList); - this.ucAddRandomActivityTask.addMultipleRandom(taskListId, listTitle); + TaskListEntity taskListEntity = this.ucSaveTaskList.save(taskList); + this.ucAddRandomActivityTask.addMultipleRandom(taskListEntity.getId(), listTitle); - return Response.created(URI.create("/task/list/" + taskListId)).build(); + return Response.created(URI.create("/task/list/" + taskListEntity.getId())).build(); } @POST @@ -197,10 +214,10 @@ public Response addExtractedIngredients(@Schema(required = true, example = """ TaskListEto taskList = new TaskListEto(); taskList.setTitle(listTitle); - Long taskListId = this.ucSaveTaskList.save(taskList); - this.ucAddRandomActivityTask.addExtractedIngredients(taskListId, recipe); + TaskListEntity taskListEntity = this.ucSaveTaskList.save(taskList); + this.ucAddRandomActivityTask.addExtractedIngredients(taskListEntity.getId(), recipe); - return Response.created(URI.create("/task/list/" + taskListId)).build(); + return Response.created(URI.create("/task/list/" + taskListEntity.getId())).build(); } /** @@ -217,11 +234,11 @@ public Response addExtractedIngredients(@Schema(required = true, example = """ @APIResponse(responseCode = "500", description = "Server unavailable or a server-side error occurred") public Response saveTaskItem(@Valid TaskItemEto item) { - Long taskItemId = this.ucSaveTaskItem.save(item); - if (item.getId() == null || item.getId() != taskItemId) { - return Response.created(URI.create("/task/item/" + taskItemId)).entity(taskItemId).build(); + TaskItemEntity savedTaskItem = this.ucSaveTaskItem.save(item); + if (item.getId() == null || !Objects.equals(item.getId(), savedTaskItem.getId())) { + return Response.created(URI.create("/task/item/" + savedTaskItem.getId())).entity(savedTaskItem.getId()).build(); } - return Response.ok(taskItemId).build(); + return Response.ok(savedTaskItem.getVersion()).build(); } /** diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 7c9b093..0d4eff5 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -10,7 +10,7 @@ quarkus.flyway.create-schemas=true quarkus.flyway.migrate-at-start=true quarkus.http.cors=true -quarkus.http.cors.origins=http://localhost:3000,http://localhost:8080 +quarkus.http.cors.origins=http://localhost:5173,http://localhost:8080 quarkus.http.cors.headers=accept, authorization, content-type, x-requested-with quarkus.http.cors.methods=GET, POST, OPTIONS, DELETE @@ -18,6 +18,7 @@ quarkus.http.cors.methods=GET, POST, OPTIONS, DELETE %dev.quarkus.hibernate-orm.log.sql=true %dev.quarkus.hibernate-orm.validate-in-dev-mode=true %dev.quarkus.flyway.schemas=quarkus +%dev.quarkus.http.access-log.enabled=true %test.quarkus.rest-client.logging.scope=request-response %dev.quarkus.rest-client.logging.scope=request-response diff --git a/backend/src/test/java/org/example/app/task/service/TaskServiceTest.java b/backend/src/test/java/org/example/app/task/service/TaskServiceTest.java index f6440cf..3ddb474 100644 --- a/backend/src/test/java/org/example/app/task/service/TaskServiceTest.java +++ b/backend/src/test/java/org/example/app/task/service/TaskServiceTest.java @@ -7,6 +7,8 @@ import org.assertj.core.api.BDDAssertions; import org.example.app.task.common.TaskItemEto; import org.example.app.task.common.TaskListCto; +import org.example.app.task.dataaccess.TaskItemEntity; +import org.example.app.task.dataaccess.TaskListEntity; import org.example.app.task.logic.UcAddRandomActivityTaskItem; import org.example.app.task.logic.UcDeleteTaskItem; import org.example.app.task.logic.UcDeleteTaskList; @@ -68,8 +70,10 @@ class Post { @Test void shouldCallSaveUseCaseAndReturn204WhenCreatingTaskList() { + TaskListEntity taskListEntity = new TaskListEntity(); + taskListEntity.setId(123L); - given(TaskServiceTest.this.saveTaskList.save(Mockito.any())).willReturn(123L); + given(TaskServiceTest.this.saveTaskList.save(Mockito.any())).willReturn(taskListEntity); given().when().body("{ \"title\": \"Shopping List\" }").contentType(ContentType.JSON).post("/task/list").then() .statusCode(201); @@ -179,6 +183,11 @@ class MultipleRandomActivities { class Post { @Test void shouldCallRandomActivitiesUseCaseAndReturn201() { + TaskListEntity taskListEntity = new TaskListEntity(); + taskListEntity.setId(123L); + taskListEntity.setTitle("Shopping list"); + + given(TaskServiceTest.this.saveTaskList.save(Mockito.any())).willReturn(taskListEntity); given().when().body("Shopping list").contentType(ContentType.TEXT).post("/task/list/multiple-random-activities").then().statusCode(201); then(TaskServiceTest.this.addRandomActivityTaskItem).should().addMultipleRandom(anyLong(), anyString()); @@ -202,6 +211,11 @@ class IngredientList { class Post { @Test void shouldCallRandomActivitiesUseCaseAndReturn201() { + TaskListEntity taskListEntity = new TaskListEntity(); + taskListEntity.setId(123L); + taskListEntity.setTitle("Shopping list"); + + given(TaskServiceTest.this.saveTaskList.save(Mockito.any())).willReturn(taskListEntity); given().when().body("{\"listTitle\": \"Shopping list\", \"recipe\": \"Take flour, sugar and chocolate and mix everything.\"}") .contentType(ContentType.JSON).post("/task/list/ingredient-list").then().statusCode(201); @@ -234,8 +248,10 @@ class Post { @Test void shouldCallSaveUseCaseAndReturn201WhenCreatingTaskItem() { + TaskItemEntity taskItemEntity = new TaskItemEntity(); + taskItemEntity.setId(42L); - given(TaskServiceTest.this.saveTaskItem.save(Mockito.any())).willReturn(42L); + given(TaskServiceTest.this.saveTaskItem.save(Mockito.any())).willReturn(taskItemEntity); given().when().body("{ \"title\": \"Buy Milk\", \"taskListId\": 123 }").contentType(ContentType.JSON) .post("/task/item").then().statusCode(201).body(is("42")); diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..3bc9884 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,19 @@ + + + + + + + + + + + + My Todos + + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json index be870a9..4425a07 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,21 +6,27 @@ "@material-ui/core": "^4.11.2", "@material-ui/icons": "^4.11.2", "@material-ui/lab": "^4.0.0-alpha.57", + "@tailwindcss/vite": "^4.0.14", + "@types/react-big-calendar": "^1.16.1", "darkreader": "^4.9.26", "http-proxy-middleware": "^2.0.6", + "lucide-react": "^0.479.0", + "moment": "^2.30.1", "react": "^17.0.1", "react-beautiful-dnd": "^13.0.0", + "react-big-calendar": "^1.18.0", "react-dom": "^17.0.1", "react-flip-move": "^3.0.4", "react-scripts": "^5.0.1", + "tailwindcss": "^4.0.14", "typescript": "^4.1.3", "uuid": "^7.0.3", "wouter": "^2.7.0" }, "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", - "eject": "react-scripts eject" + "start": "vite", + "build": "vite build", + "serve": "vite preview" }, "eslintConfig": { "extends": "react-app" @@ -39,13 +45,10 @@ }, "devDependencies": { "@types/material-ui": "^0.21.8", - "@types/node": "^14.14.16", "@types/react": "^17.0.1", "@types/react-beautiful-dnd": "^13.0.0", - "@types/react-dom": "^17.0.1" - }, - "resolutions": { - "@types/react": "17.0.1", - "@types/react-dom": "17.0.1" + "@types/react-dom": "^17.0.1", + "@vitejs/plugin-react": "^4.3.4", + "vite": "^6.2.1" } } diff --git a/frontend/public/index.html b/frontend/public/index.html deleted file mode 100644 index 0f2b8a1..0000000 --- a/frontend/public/index.html +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - My Todos - - - -
- - - diff --git a/frontend/setupProxy.js b/frontend/setupProxy.js deleted file mode 100644 index d807abd..0000000 --- a/frontend/setupProxy.js +++ /dev/null @@ -1,16 +0,0 @@ -const { createProxyMiddleware } = require('http-proxy-middleware'); - -const proxy = { - target: 'http://localhost:8080', - changeOrigin: true, - pathRewrite: { - '^/api/': '/' // remove base path - } -}; - -module.exports = function(app) { - app.use( - '/api', - createProxyMiddleware(proxy) - ); -}; \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b6e5ca1..0b2b42e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,28 +1,65 @@ -import {useContext} from "react"; -import {Route} from "wouter"; -import AddTodo from "./components/Todos/AddTodo"; -import PersistentDrawerLeft from "./components/PersistentDrawerLeft"; -import Todos from "./components/Todos/Todos"; -import {MainContext} from "./context/MainContext"; -import About from "./pages/About"; -import Settings from "./pages/Settings"; +import { Snackbar } from "@material-ui/core"; +import { Alert } from "@material-ui/lab"; +import { useContext } from "react"; +import { Route } from "wouter"; +import CalendarView from "./components/calendar"; +import Header from "./components/misc/header"; +import Sidebar from "./components/misc/sidebar"; +import Todos from "./components/todos/todos"; +import { MainContext } from "./provider/mainProvider"; function App() { - const { addTodo } = useContext(MainContext)!; + const { + errorAlert, + setErrorAlert, + successAlert, + setSuccessAlert, + showCalendar, + } = useContext(MainContext)!; return ( -
- - - - - - - - - - - +
+
+
+ + + {showCalendar ? : } + +
+ setErrorAlert("")} + anchorOrigin={{ vertical: "bottom", horizontal: "right" }} + > + } + elevation={6} + variant="filled" + onClose={() => setErrorAlert("")} + severity="error" + > + {errorAlert} + + + { + setSuccessAlert(""); + }} + anchorOrigin={{ vertical: "bottom", horizontal: "right" }} + > + } + elevation={6} + variant="filled" + //onClose={() => setSuccessAlert("")} + severity="success" + > + {successAlert} + +
); } diff --git a/frontend/src/components/Actions/ActionsMenu.tsx b/frontend/src/components/Actions/ActionsMenu.tsx deleted file mode 100644 index b06f892..0000000 --- a/frontend/src/components/Actions/ActionsMenu.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import {TaskItemType} from "../../types"; -import {IconButton, Menu, MenuItem, Typography} from "@material-ui/core"; -import {DeleteTwoTone as DeleteIcon, EditTwoTone as EditIcon, Star as StarIcon, StarTwoTone as StarIconOutlined, SvgIconComponent,} from "@material-ui/icons"; -import useChangeMenuIcon from "../../hooks/useChangeMenuIcon"; -import React, {useState} from "react"; - -const ITEM_HEIGHT = 48; - -interface Option { - name: string; - customColor?: string | undefined; - iconColor?: - | "error" - | "action" - | "inherit" - | "disabled" - | "primary" - | "secondary" - | undefined; - textColor?: - | "inherit" - | "initial" - | "error" - | "primary" - | "secondary" - | "textPrimary" - | "textSecondary" - | undefined; - icon: SvgIconComponent; - method: (e?: React.MouseEvent) => void; -} - -interface Props { - deleteTodo: (e: any) => void; - setEditOpen: React.Dispatch>; - markStar: (id: number) => void; - todo: TaskItemType; -} - -enum OptionName { - STAR = "Star", - UNSTAR = "Unstar", - EDIT = "Edit", - DELETE = "Delete", -} - -export default function ActionsMenu({ - deleteTodo, - setEditOpen, - markStar, - todo, -}: Props) { - const [anchorEl, setAnchorEl] = useState< - (EventTarget & HTMLButtonElement) | null - >(null); - const open = Boolean(anchorEl); - const MenuIcon = useChangeMenuIcon(); - - const options: Option[] = [ - { - name: todo.starred ? OptionName.UNSTAR : OptionName.STAR, - customColor: todo.starred ? "#e7aa2a" : "#e7aa2a", - icon: todo.starred ? StarIcon : StarIconOutlined, - method: () => { - markStar(todo.id); - setAnchorEl(null); - }, - }, - { - name: OptionName.EDIT, - iconColor: "primary", - textColor: "primary", - icon: EditIcon, - method: () => { - setEditOpen(true); - setAnchorEl(null); - }, - }, - { - name: OptionName.DELETE, - iconColor: "error", - textColor: "error", - icon: DeleteIcon, - method: (e) => { - deleteTodo(e); - setAnchorEl(null); - }, - }, - ]; - const handleClick = (e: React.MouseEvent) => { - setAnchorEl(e.currentTarget); - }; - - const handleEvent = (option: OptionName, e: any) => { - if (option === "Star") markStar(todo.id); - else if (option === "Edit") setEditOpen(true); - else if (option === "Delete") deleteTodo(e); - setAnchorEl(null); - }; - - return ( -
- - - - - {options.map((option) => ( - - -   - - {option.name} - - - ))} - -
- ); -} diff --git a/frontend/src/components/Actions/DeleteConfirm.tsx b/frontend/src/components/Actions/DeleteConfirm.tsx deleted file mode 100644 index e7c339e..0000000 --- a/frontend/src/components/Actions/DeleteConfirm.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, - Button, - Divider, - useMediaQuery, -} from "@material-ui/core"; - -interface Props { - open: boolean; - close: () => void; - yes: () => void; -} - -export const DeleteConfirm = ({ open, close, yes }: Props) => { - const matches = useMediaQuery("(max-width: 768px)"); - return ( - - DELETE ITEM? - - - Are you sure you want to delete this item? - -
- -
- - PROTIP: -
- You can hold down shift when clicking the delete button to - bypass this confirmation entirely -
-
-
- - - - -
- ); -}; - -export const DeleteAllConfirm = ({ open, close, yes }: Props) => { - return ( - - DELETE ALL ITEMS? - - - Are you sure you want to delete all items? - - - - - - - - ); -}; diff --git a/frontend/src/components/Actions/EditConfirm.tsx b/frontend/src/components/Actions/EditConfirm.tsx deleted file mode 100644 index 2a2637c..0000000 --- a/frontend/src/components/Actions/EditConfirm.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { useState } from "react"; -import { - Dialog, - DialogTitle, - DialogActions, - DialogContent, - DialogContentText, - TextField, - Button, -} from "@material-ui/core"; - -interface Props { - yes: (val: string) => void; - open: boolean; - close: () => void; - value: string; -} - -const EditConfirm = ({ open, close, value, yes }: Props) => { - const [newValue, setNewValue] = useState(value); - const onClose = () => { - setNewValue(value); - close(); - }; - return ( - - EDIT ITEM - - - Please provide the new name for this item. - - setNewValue(e.target.value)} - /> - - - - - - - ); -}; - -export default EditConfirm; diff --git a/frontend/src/components/Actions/MoreMenu.tsx b/frontend/src/components/Actions/MoreMenu.tsx deleted file mode 100644 index 9c96a3b..0000000 --- a/frontend/src/components/Actions/MoreMenu.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import React, { useState, useContext } from "react"; -import { IconButton, Menu, MenuItem, Typography } from "@material-ui/core"; -import { - DeleteSweepTwoTone as DeleteSweepIcon, - SvgIconComponent, -} from "@material-ui/icons"; -import useChangeMenuIcon from "../../hooks/useChangeMenuIcon"; -import { MainContext } from "../../context/MainContext"; -import { DeleteAllConfirm } from "./DeleteConfirm"; - -const MoreMenu = () => { - const [anchorEl, setAnchorEl] = useState< - (EventTarget & HTMLButtonElement) | null - >(null); - const [deleteOpen, setDeleteOpen] = useState(false); - const open = Boolean(anchorEl); - const MenuIcon = useChangeMenuIcon(); - const { todos, deleteAll } = useContext(MainContext)!; - - const handleClick = (e: React.MouseEvent) => - setAnchorEl(e.currentTarget); - const handleClose = () => setAnchorEl(null); - - interface Option { - name: string; - iconColor: - | "error" - | "action" - | "inherit" - | "disabled" - | "primary" - | "secondary" - | undefined; - textColor: - | "error" - | "inherit" - | "primary" - | "secondary" - | "initial" - | "textPrimary" - | "textSecondary" - | undefined; - disabled: boolean; - icon: SvgIconComponent; - method: () => void; - } - - const options: Option[] = [ - { - name: "Delete All", - iconColor: "error", - textColor: "error", - disabled: todos.length === 0, - icon: DeleteSweepIcon, - method: () => { - handleClose(); - setDeleteOpen(true); - }, - }, - ]; - - return ( -
- - - - - {options.map((option) => ( - -   - {option.name} - - ))} - - { - setDeleteOpen(false); - setTimeout(() => deleteAll(), 200); - }} - open={deleteOpen} - close={() => setDeleteOpen(false)} - /> -
- ); -}; - -export default MoreMenu; diff --git a/frontend/src/components/Dialogs/dialogBase.tsx b/frontend/src/components/Dialogs/dialogBase.tsx new file mode 100644 index 0000000..9f98592 --- /dev/null +++ b/frontend/src/components/Dialogs/dialogBase.tsx @@ -0,0 +1,25 @@ +import { ReactNode, RefObject, useRef } from "react"; +import useOutsideClick from "../../hooks/outsideClick"; + +interface DialogBaseI { + children: ReactNode; + show: boolean; + close: () => void; +} + +const DialogBase = ({ children, show, close }: DialogBaseI) => { + const refObject = useRef(null); + useOutsideClick([refObject as RefObject], () => close(), show); + + return ( +
+
{children}
+
+ ); +}; + +export default DialogBase; diff --git a/frontend/src/components/Dialogs/todo/addTodo.tsx b/frontend/src/components/Dialogs/todo/addTodo.tsx new file mode 100644 index 0000000..78b21f6 --- /dev/null +++ b/frontend/src/components/Dialogs/todo/addTodo.tsx @@ -0,0 +1,71 @@ +import { X } from "lucide-react"; +import { Dispatch, SetStateAction, useContext, useState } from "react"; +import { TodoContext } from "../../../provider/todoProvider"; +import DialogBase from "../dialogBase"; + +interface AddTodoI { + open: boolean; + close: () => void; +} + +const AddTodo = ({ open, close }: AddTodoI) => { + const { addTodo } = useContext(TodoContext)!; + const [title, setTitle] = useState("New Item"); + const [deadline, setDeadline] = useState(null) as [ + string | null, + Dispatch> + ]; + + const onClose = () => { + setTitle("New Item"); + setDeadline("reset"); + //Necessary to clean date input if not filled out fully + setTimeout(() => { + setDeadline(null); + }, 100); + close(); + }; + + return ( + +
+
+

+ Add Todo +

+ +
+ Title + setTitle(e.target.value)} + /> + Date (optional) + { + setDeadline(0 === e.target.value.length ? null : e.target.value); + }} + /> + +
+
+ ); +}; + +export default AddTodo; diff --git a/frontend/src/components/Dialogs/todo/deleteTodoConfirm.tsx b/frontend/src/components/Dialogs/todo/deleteTodoConfirm.tsx new file mode 100644 index 0000000..cd1883b --- /dev/null +++ b/frontend/src/components/Dialogs/todo/deleteTodoConfirm.tsx @@ -0,0 +1,40 @@ +import DialogBase from "../dialogBase"; + +interface DeleteTodoConfirmI { + open: boolean; + close: () => void; + yes: () => void; +} + +export const DeleteTodoConfirm = ({ open, close, yes }: DeleteTodoConfirmI) => { + return ( + +
+

DELETE ITEM?

+

+ Are you sure you want to delete this item? +

+

+ PROTIP: +
+ You can hold down shift when clicking the delete button to + bypass this confirmation entirely +

+
+ + +
+
+
+ ); +}; diff --git a/frontend/src/components/Dialogs/todo/editTodo.tsx b/frontend/src/components/Dialogs/todo/editTodo.tsx new file mode 100644 index 0000000..ceabca3 --- /dev/null +++ b/frontend/src/components/Dialogs/todo/editTodo.tsx @@ -0,0 +1,61 @@ +import { X } from "lucide-react"; +import { useState } from "react"; +import DialogBase from "../dialogBase"; + +interface EditTodoI { + yes: (newTitle: string, newDeadline: string | null) => void; + open: boolean; + close: () => void; + title: string; + deadline: string | null; +} + +const EditTodo = ({ open, close, title, deadline, yes }: EditTodoI) => { + const [newTitle, setNewTitle] = useState(title); + const [newDeadline, setNewDeadline] = useState(deadline); + const onClose = () => { + setNewTitle(title); + setNewDeadline(deadline); + close(); + }; + + return ( + +
+
+

+ Todo +

+ +
+ Title + setNewTitle(e.target.value)} + /> + Date (optional) + { + setNewDeadline(0 === e.target.value.length ? null : e.target.value); + }} + /> + +
+
+ ); +}; + +export default EditTodo; diff --git a/frontend/src/components/Dialogs/todoList/addTodoList.tsx b/frontend/src/components/Dialogs/todoList/addTodoList.tsx new file mode 100644 index 0000000..d9cc128 --- /dev/null +++ b/frontend/src/components/Dialogs/todoList/addTodoList.tsx @@ -0,0 +1,61 @@ +import { Sparkles, X } from "lucide-react"; +import { useContext, useState } from "react"; +import { TodoListContext } from "../../../provider/todoListProvider"; +import DialogBase from "../dialogBase"; + +interface AddTodoListI { + open: boolean; + close: () => void; +} + +const AddTodoList = ({ open, close }: AddTodoListI) => { + const { addTaskList, generateRandomList } = useContext(TodoListContext)!; + const [title, setTitle] = useState("New List"); + const onClose = () => { + setTitle("New List"); + close(); + }; + + return ( + +
+
+

+ Todo List +

+ +
+ Title + setTitle(e.target.value)} + /> +
+ + +
+
+
+ ); +}; + +export default AddTodoList; diff --git a/frontend/src/components/Dialogs/todoList/deleteTodoListConfirm.tsx b/frontend/src/components/Dialogs/todoList/deleteTodoListConfirm.tsx new file mode 100644 index 0000000..3ab9a26 --- /dev/null +++ b/frontend/src/components/Dialogs/todoList/deleteTodoListConfirm.tsx @@ -0,0 +1,44 @@ +import DialogBase from "../dialogBase"; + +interface DeleteTodoListConfirmI { + open: boolean; + close: () => void; + yes: () => void; +} + +export const DeleteTodoListConfirm = ({ + open, + close, + yes, +}: DeleteTodoListConfirmI) => { + return ( + +
+

DELETE LIST?

+

+ Are you sure you want to delete this list? +

+

+ PROTIP: +
+ You can hold down shift when clicking the delete button to + bypass this confirmation entirely +

+
+ + +
+
+
+ ); +}; diff --git a/frontend/src/components/Dialogs/todoList/editTodoList.tsx b/frontend/src/components/Dialogs/todoList/editTodoList.tsx new file mode 100644 index 0000000..bcf0109 --- /dev/null +++ b/frontend/src/components/Dialogs/todoList/editTodoList.tsx @@ -0,0 +1,53 @@ +import { X } from "lucide-react"; +import { useEffect, useState } from "react"; +import DialogBase from "../dialogBase"; + +interface EditTodoListI { + yes: (newTitle: string) => void; + open: boolean; + close: () => void; + title: string; +} + +const EditTodoList = ({ open, close, title, yes }: EditTodoListI) => { + const [newTitle, setNewTitle] = useState(title); + const onClose = () => { + setNewTitle(title); + close(); + }; + + useEffect(() => { + setNewTitle(title); + }, [title]); + + return ( + +
+
+

+ Todo List +

+ +
+ Title + setNewTitle(e.target.value)} + /> + +
+
+ ); +}; + +export default EditTodoList; diff --git a/frontend/src/components/PersistentDrawerLeft.tsx b/frontend/src/components/PersistentDrawerLeft.tsx deleted file mode 100644 index 0161c2e..0000000 --- a/frontend/src/components/PersistentDrawerLeft.tsx +++ /dev/null @@ -1,275 +0,0 @@ -import { - AppBar, - CssBaseline, - Divider, - Drawer, - IconButton, - List, - ListItem, - ListItemIcon, - ListItemText, - makeStyles, - useTheme, - Toolbar, - Typography, - Slide, - Button, - useScrollTrigger, - useMediaQuery, -} from "@material-ui/core"; -import { - ChevronLeft as ChevronLeftIcon, - ChevronRight as ChevronRightIcon, - NotesOutlined as TodoIcon, - InfoOutlined as AboutIconOutlined, - Info as AboutIcon, - Menu as MenuIcon, - ArrowBack as BackIcon, - SettingsOutlined as SettingsIconOutlined, - Settings as SettingsIcon, -} from "@material-ui/icons"; -import { Link, useLocation } from "wouter"; -import clsx from "clsx"; -import { useState } from "react"; -import CustomLink from "./CustomLink"; -import MoreMenu from "./Actions/MoreMenu"; - -const drawerWidth = 240; - -const useStyles = makeStyles((theme) => ({ - root: { - display: "flex", - }, - appBar: { - transition: theme.transitions.create(["margin", "width"], { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), - }, - appBarShift: { - width: `calc(100% - ${drawerWidth}px)`, - marginLeft: drawerWidth, - transition: theme.transitions.create(["margin", "width"], { - easing: theme.transitions.easing.easeOut, - duration: theme.transitions.duration.enteringScreen, - }), - }, - menuButton: { - marginRight: theme.spacing(2), - }, - hide: { - display: "none", - }, - drawer: { - width: drawerWidth, - flexShrink: 0, - }, - drawerPaper: { - width: drawerWidth, - }, - drawerHeader: { - display: "flex", - alignItems: "center", - padding: theme.spacing(0, 1), - // necessary for content to be below app bar - ...theme.mixins.toolbar, - justifyContent: "flex-end", - }, - content: { - flexGrow: 1, - padding: theme.spacing(1), - transition: theme.transitions.create("margin", { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, - }), - marginLeft: -drawerWidth, - }, - contentShift: { - transition: theme.transitions.create("margin", { - easing: theme.transitions.easing.easeOut, - duration: theme.transitions.duration.enteringScreen, - }), - marginLeft: 0, - }, -})); - -interface Props { - window?: () => Window; - children: React.ReactElement; -} - -function HideOnScroll(props: Props) { - const { children, window } = props; - // Note that you normally won't need to set the window ref as useScrollTrigger - // will default to window. - // This is only being set here because the demo is in an iframe. - const trigger = useScrollTrigger({ target: window ? window() : undefined }); - - return ( - - {children} - - ); -} - -export default function PersistentDrawerLeft(props: any) { - const classes = useStyles(); - const theme = useTheme(); - const [open, setOpen] = useState(false); - const [location] = useLocation(); - const matches = useMediaQuery("(max-width: 768px)"); - const handleDrawerOpen = () => { - setOpen(true); - }; - - const handleDrawerClose = () => { - setOpen(false); - }; - - return ( -
- - - - - {matches ? ( - location === "/" ? ( - - - - ) : ( - - - - - - ) - ) : ( - "" - )} - {!open && ( - <> - {matches ? ( - - {location === "/" - ? "MY TODOS" - : location.toUpperCase().replace("/", "")} - - ) : ( - - - MY TODOS - - - )} - - {!matches && ( - <> - {["Settings", "About"].map((name, i) => ( - - - - ))} - - )} - {location === "/" && } - - )} - - - - -
- - {theme.direction === "ltr" ? ( - - ) : ( - - )} - -
- - - {["Todos", "Settings", "About"].map((text, index) => ( - - - - {index === 0 ? ( - - ) : index === 1 ? ( - - ) : ( - - )} - - - - - ))} - -
-
-
-
-
- ); -} diff --git a/frontend/src/components/Todos/AddTodo.tsx b/frontend/src/components/Todos/AddTodo.tsx deleted file mode 100644 index b17600a..0000000 --- a/frontend/src/components/Todos/AddTodo.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { useState, FC, ChangeEvent } from "react"; -import { - FormControl, - Container, - Button, - TextField, - Snackbar, -} from "@material-ui/core"; -import { Alert } from "@material-ui/lab"; -import { Add } from "@material-ui/icons"; - -const AddTodo: FC<{ addTodo: (text: string) => void }> = ({ addTodo }) => { - const [text, setText] = useState(""); - const [open, setOpen] = useState(false); - const handleChange = ( - e: ChangeEvent - ) => setText(e.target.value); - const createTodo = (e: React.FormEvent) => { - e.preventDefault(); - addTodo(text); - setText(""); - if (text.trim()) setOpen(true); - }; - - return ( -
- -
- - - - -
-
- setOpen(false)} - anchorOrigin={{ vertical: "bottom", horizontal: "right" }} - > - } - elevation={6} - variant="filled" - onClose={() => setOpen(false)} - severity="success" - > - Successfully added item! - - -
- ); -}; - -export default AddTodo; diff --git a/frontend/src/components/Todos/Todo.tsx b/frontend/src/components/Todos/Todo.tsx index 3567d3d..c23ee01 100644 --- a/frontend/src/components/Todos/Todo.tsx +++ b/frontend/src/components/Todos/Todo.tsx @@ -1,143 +1,111 @@ -import {TaskItemType} from "../../types"; -import React, {forwardRef, useContext, useState} from "react"; -import {DeleteConfirm} from "../Actions/DeleteConfirm"; -import EditConfirm from "../Actions/EditConfirm"; -import {Card, CardContent, Checkbox, Container, Grid, Typography, useMediaQuery,} from "@material-ui/core"; -import {Draggable} from "react-beautiful-dnd"; -import {DeleteConfirmContext} from "../../context/DeleteConfirmContext"; -import ActionsMenu from "../Actions/ActionsMenu"; -import {SmallTextContext} from "../../context/SmallTextContext"; -import {ThemeContext} from "../../context/ThemeContext"; -import {MainContext} from "../../context/MainContext"; +import { CalendarDays, Edit2, Star, StarOff, Trash } from "lucide-react"; +import { forwardRef, RefObject, useContext, useRef, useState } from "react"; +import useDisableSelect from "../../hooks/disableSelect"; +import useShowOnHover from "../../hooks/showOnHover"; +import { DeleteConfirmContext } from "../../provider/deleteConfirmProvider"; +import { TodoContext } from "../../provider/todoProvider"; +import { TaskItemTypeI } from "../../types/types"; +import { DeleteTodoConfirm } from "../dialogs/todo/deleteTodoConfirm"; +import EditTodo from "../dialogs/todo/editTodo"; +import Checkbox from "../utils/checkbox"; -interface Props { - todo: TaskItemType; - index: number; +interface TodoI { + todo: TaskItemTypeI; onDelete: () => void; onEdit: () => void; } -const Todo = forwardRef( - ({ todo, index, onDelete, onEdit }: Props, ref: any) => { - const { markComplete, delTodo, editTodo, markStar } = useContext( - MainContext - )!; - const matches = useMediaQuery("(max-width: 768px)"); - const [deleteOpen, setDeleteOpen] = useState(false); - const [editOpen, setEditOpen] = useState(false); - const { isDeleteConfirmation } = useContext(DeleteConfirmContext)!; - const { isSmallText } = useContext(SmallTextContext)!; - const { isDark } = useContext(ThemeContext)!; - let checkedStyle = { textDecoration: "none" }; - if (todo.completed) checkedStyle.textDecoration = "line-through"; - else checkedStyle.textDecoration = "none"; +const Todo = forwardRef(({ todo, onDelete, onEdit }: TodoI, ref: any) => { + const { markComplete, delTodo, editTodo, markStar } = + useContext(TodoContext)!; + const [deleteOpen, setDeleteOpen] = useState(false); + const [editOpen, setEditOpen] = useState(false); + const { isDeleteConfirmation } = useContext(DeleteConfirmContext)!; - const styles: any = { - card: { - marginTop: matches ? 20 : 35, - }, - icon: { - float: "right", - paddingTop: "10px", - }, - text: { - wordBreak: "break-word", - display: "-webkit-box", - WebkitLineClamp: 2, - WebkitBoxOrient: "vertical", - overflow: "hidden", - fontWeight: todo.starred ? 600 : "normal", - fontSize: matches ? "17px" : isSmallText ? "17px" : "24px", - color: "", - }, - }; + const todoContainerRef: RefObject = useRef(null); + const todoOptionsRef: RefObject = useRef(null); - if (todo.starred) { - styles.text.color = isDark ? "#ffe066" : "#3f51b5"; - } + useShowOnHover(todoContainerRef, todoOptionsRef); + useDisableSelect(todoContainerRef); - const deleteTodo = (e: any) => { - if (e.shiftKey || isDeleteConfirmation) { - delTodo(todo.id); - onDelete(); - } else setDeleteOpen(true); - }; - return ( - - - {(p) => ( - { + if (e.shiftKey || isDeleteConfirmation) { + delTodo(todo.id); + onDelete(); + } else setDeleteOpen(true); + }; + + return ( +
+
+
+ markComplete(todo.id)} + /> +
+

{todo.title}

+
- - - - - markComplete(todo.id)} - centerRipple={false} - /> - - -
{todo.title}
-
- - deleteTodo(e)} - setEditOpen={setEditOpen} - todo={todo} - markStar={markStar} - /> - -
-
-
- - )} - - { - setDeleteOpen(false); - setTimeout(() => { - delTodo(todo.id); - onDelete(); - }, 200); - }} - open={deleteOpen} - close={() => setDeleteOpen(false)} - /> - { - setEditOpen(false); - setTimeout(() => { - editTodo(todo.id, val); - onEdit(); - }, 200); - }} - open={editOpen} - close={() => setEditOpen(false)} - value={todo.title} - /> - - ); - } -); + +

+ {new Date(todo.deadline ?? "").toLocaleString()} +

+
+
+
+
+ setEditOpen(true)} + /> + markStar(todo.id)}> + {!todo.starred ? ( + + ) : ( + + )} + + deleteTodo(e)} + /> +
+
+ { + setDeleteOpen(false); + delTodo(todo.id); + onDelete(); + }} + open={deleteOpen} + close={() => setDeleteOpen(false)} + /> + { + setEditOpen(false); + editTodo(todo.id, newTitle, newDeadline); + onEdit(); + }} + open={editOpen} + close={() => { + setEditOpen(false); + }} + title={todo.title} + deadline={todo.deadline?.substring(0, 16) ?? null} + /> +
+ ); +}); export default Todo; diff --git a/frontend/src/components/Todos/Todos.tsx b/frontend/src/components/Todos/Todos.tsx index 42bd1c1..6253f9d 100644 --- a/frontend/src/components/Todos/Todos.tsx +++ b/frontend/src/components/Todos/Todos.tsx @@ -1,81 +1,191 @@ -import { useContext, useState } from "react"; -import { MainContext } from "../../context/MainContext"; -import { Droppable, DragDropContext, DropResult } from "react-beautiful-dnd"; -import Todo from "./Todo"; -import { Snackbar } from "@material-ui/core"; -import { Alert } from "@material-ui/lab"; +import { Edit2, Filter, ListFilter, Plus, Sparkles } from "lucide-react"; +import { useContext, useEffect, useRef, useState } from "react"; import FlipMove from "react-flip-move"; +import { useRoute } from "wouter"; +import useHasOverflow from "../../hooks/hasOverflow"; +import useScrollbarWidth from "../../hooks/scrollBarWidth"; +import { TodoListContext } from "../../provider/todoListProvider"; +import { TodoContext } from "../../provider/todoProvider"; +import { TaskItemTypeI } from "../../types/types"; +import AddTodo from "../dialogs/todo/addTodo"; +import EditTodoList from "../dialogs/todoList/editTodoList"; +import { FilterMenu, SelectedFilterE } from "../menus/filterMenu"; +import { SelectedSortE, SelectedSortOrderE, SortMenu } from "../menus/sortMenu"; +import Todo from "./todo"; const Todos = () => { - const { todos, moveTodo } = useContext(MainContext)!; - const [deleteSnackOpen, setDeleteSnackOpen] = useState(false); - const [editSnackOpen, setEditSnackOpen] = useState(false); - const [dragging, setDragging] = useState(false); - const onDragEnd = (x: DropResult) => { - if (!x.destination) return console.log(x); - moveTodo(x.source.index, x.destination.index); - setTimeout(() => setDragging(false), 200); - }; + const { taskLists, editTodoList } = useContext(TodoListContext)!; + const { todos, addRandomTodo } = useContext(TodoContext)!; + const [, params] = useRoute("/:listId"); + const listId = params?.listId; + + const [showAddTodoOpen, setShowAddTodoOpen] = useState(false); + const [showEditTodoList, setShowEditTodoList] = useState(false); + + const taskListTitle = + taskLists.filter((e) => listId && e.id === +listId)[0]?.title ?? + (undefined === listId ? "Please select a list" : "Loading..."); + return ( - <> - setDragging(true)} - onDragEnd={onDragEnd} - > - - {(p) => ( -
- - {todos.map((todo, i) => { - return ( - setDeleteSnackOpen(true)} - index={i} - onEdit={() => setEditSnackOpen(true)} - /> - ); - })} - - {p.placeholder} -
- )} -
-
- - setDeleteSnackOpen(false)} - anchorOrigin={{ vertical: "bottom", horizontal: "right" }} - > - setDeleteSnackOpen(false)} - severity="success" +
+
+
+

{taskListTitle}

+ setShowEditTodoList(true)} + /> +
+
+ addRandomTodo()} + /> + setShowAddTodoOpen(true)} + /> +
+
+
+ !e.completed)} /> + e.completed)} /> +
+ + setShowAddTodoOpen(false)} /> + setShowEditTodoList(false)} + yes={(newTitle: string) => { + setShowEditTodoList(false); + editTodoList(newTitle); + }} + title={taskListTitle} + > +
+ ); +}; + +interface ListI { + title: string; + todos: TaskItemTypeI[]; +} + +function List({ title, todos }: ListI) { + const { applyFilter, applySort } = useContext(TodoContext)!; + const scrollContainerRef = useRef(null); + const scrollbarWidth = useScrollbarWidth(); + const hasOverflow = useHasOverflow(scrollContainerRef); + + const [, setDeleteSnackOpen] = useState(false); + const [, setEditSnackOpen] = useState(false); + + /** + * + * APPLY FILTER + * + */ + + const filterRef = useRef(null); + const [showFilter, setShowFilter] = useState(false); + const [selectedFilter, setSelectedFilter] = useState(SelectedFilterE.NONE); + + const [filteredTodos, setFilteredTodos] = useState([] as TaskItemTypeI[]); + + useEffect(() => { + setFilteredTodos(applyFilter(todos, selectedFilter)); + }, [applyFilter, selectedFilter, todos]); + + /** + * + * APPLY SORT ON FILTERED ITEMS + * + */ + + const sortRef = useRef(null); + const [showSort, setShowSort] = useState(false); + const [selectedSort, setSelectedSort] = useState({ + selectedSort: SelectedSortE.NONE, + selectedSortOrder: SelectedSortOrderE.NOT_SELECTED, + }); + + const [sortedTodos, setSortedTodos] = useState([] as TaskItemTypeI[]); + + useEffect(() => { + setSortedTodos(applySort(filteredTodos, selectedSort)); + }, [applySort, selectedSort, filteredTodos]); + + return ( +
+
+

{`${title} - ${sortedTodos.length}`}

+
- Successfully deleted item! - - - setEditSnackOpen(false)} - anchorOrigin={{ vertical: "bottom", horizontal: "right" }} + setShowFilter(!showFilter)} + /> + setShowSort(!showSort)} + /> +
+ + +
+
- setEditSnackOpen(false)} - severity="success" - > - Successfully edited item! - - - +
+ + {sortedTodos.map((todo, i) => { + return ( + setDeleteSnackOpen(true)} + onEdit={() => setEditSnackOpen(true)} + /> + ); + })} + +
+
+
); -}; +} export default Todos; diff --git a/frontend/src/components/calendar.tsx b/frontend/src/components/calendar.tsx new file mode 100644 index 0000000..5800134 --- /dev/null +++ b/frontend/src/components/calendar.tsx @@ -0,0 +1,32 @@ +import moment from "moment"; +import { useContext } from "react"; +import { Calendar, momentLocalizer } from "react-big-calendar"; +import { TodoContext } from "../provider/todoProvider"; + +const localizer = momentLocalizer(moment); + +const CalendarView = () => { + const { todos } = useContext(TodoContext)!; + + return ( +
+ null != e.deadline) + .map((e) => { + return { + title: e.title, + start: new Date(e.deadline), + end: new Date(e.deadline), + }; + })} + style={{ height: "100%" }} + /> +
+ ); +}; + +export default CalendarView; diff --git a/frontend/src/components/menus/filterMenu.tsx b/frontend/src/components/menus/filterMenu.tsx new file mode 100644 index 0000000..428f121 --- /dev/null +++ b/frontend/src/components/menus/filterMenu.tsx @@ -0,0 +1,108 @@ +import { Check } from "lucide-react"; +import { Dispatch, RefObject, SetStateAction, useRef } from "react"; +import useOutsideClick from "../../hooks/outsideClick"; + +interface FilterMenuI { + ignoreClick: RefObject[]; + showFilter: boolean; + setShowFilter: Dispatch>; + selectedFilter: SelectedFilterE; + setSelectedFilter: Dispatch>; +} + +export enum SelectedFilterE { + NONE, + WITH_DUE_DATE, + WITHOUT_DUE_DATE, + ONLY_STARRED, + NOT_STARRED, +} + +export const FilterMenu = ({ + ignoreClick, + showFilter, + setShowFilter, + selectedFilter, + setSelectedFilter, +}: FilterMenuI) => { + const filterMenuRef = useRef(null); + useOutsideClick( + [filterMenuRef, ...ignoreClick], + () => setShowFilter(false), + showFilter + ); + + return ( +
+ + setSelectedFilter( + selectedFilter === SelectedFilterE.WITH_DUE_DATE + ? SelectedFilterE.NONE + : SelectedFilterE.WITH_DUE_DATE + ) + } + /> + + setSelectedFilter( + selectedFilter === SelectedFilterE.WITHOUT_DUE_DATE + ? SelectedFilterE.NONE + : SelectedFilterE.WITHOUT_DUE_DATE + ) + } + /> + + setSelectedFilter( + selectedFilter === SelectedFilterE.ONLY_STARRED + ? SelectedFilterE.NONE + : SelectedFilterE.ONLY_STARRED + ) + } + /> + + setSelectedFilter( + selectedFilter === SelectedFilterE.NOT_STARRED + ? SelectedFilterE.NONE + : SelectedFilterE.NOT_STARRED + ) + } + /> +
+ ); +}; + +const FilterItem = ({ + text, + isSelected, + onClick, +}: { + text: string; + isSelected: boolean; + onClick: () => void; +}) => { + return ( +
+

{text}

+ {isSelected ? : null} +
+ ); +}; diff --git a/frontend/src/components/menus/settingsMenu.tsx b/frontend/src/components/menus/settingsMenu.tsx new file mode 100644 index 0000000..3890b85 --- /dev/null +++ b/frontend/src/components/menus/settingsMenu.tsx @@ -0,0 +1,44 @@ +import { RefObject, useContext, useRef } from "react"; +import useOutsideClick from "../../hooks/outsideClick"; +import { DeleteConfirmContext } from "../../provider/deleteConfirmProvider"; +import { MainContext } from "../../provider/mainProvider"; +import { ThemeContext } from "../../provider/themeProvider"; +import Slider from "../utils/slider"; + +interface SettingsMenuI { + ignoreClick: RefObject[]; +} + +export const SettingsMenu = ({ ignoreClick }: SettingsMenuI) => { + const { showSettings, setShowSettings } = useContext(MainContext)!; + const { isDeleteConfirmation, changeDeleteConfirm } = + useContext(DeleteConfirmContext)!; + const { isDark, changeTheme } = useContext(ThemeContext)!; + + const settingsRef = useRef(null); + useOutsideClick( + [settingsRef, ...ignoreClick], + () => setShowSettings(false), + showSettings + ); + + return ( +
+
+

Darkmode

+ +
+
+

+ Disable delete confirmation +

+ +
+
+ ); +}; diff --git a/frontend/src/components/menus/sortMenu.tsx b/frontend/src/components/menus/sortMenu.tsx new file mode 100644 index 0000000..bc9f852 --- /dev/null +++ b/frontend/src/components/menus/sortMenu.tsx @@ -0,0 +1,134 @@ +import { SortAsc, SortDesc } from "lucide-react"; +import { Dispatch, RefObject, SetStateAction, useRef } from "react"; +import useOutsideClick from "../../hooks/outsideClick"; + +interface SortMenuI { + ignoreClick: RefObject[]; + showSort: boolean; + setShowSort: Dispatch>; + selectedSort: { + selectedSort: SelectedSortE; + selectedSortOrder: SelectedSortOrderE; + }; + setSelectedSort: Dispatch< + SetStateAction<{ + selectedSort: SelectedSortE; + selectedSortOrder: SelectedSortOrderE; + }> + >; +} + +export enum SelectedSortE { + NONE, + BY_CREATION_DATE, + ALPHABETICALLY, + BY_DUE_DATE, + STARRED, +} + +export enum SelectedSortOrderE { + NOT_SELECTED, + ASC, + DESC, +} + +export const SortMenu = ({ + ignoreClick, + showSort, + setShowSort, + selectedSort, + setSelectedSort, +}: SortMenuI) => { + const sortMenuRef = useRef(null); + useOutsideClick( + [sortMenuRef, ...ignoreClick], + () => setShowSort(false), + showSort + ); + + const onClick = (clickedSort: SelectedSortE) => { + if ( + selectedSort.selectedSortOrder === SelectedSortOrderE.NOT_SELECTED || + selectedSort.selectedSort !== clickedSort + ) { + setSelectedSort({ + selectedSort: clickedSort, + selectedSortOrder: SelectedSortOrderE.ASC, + }); + } else if (selectedSort.selectedSortOrder === SelectedSortOrderE.ASC) { + setSelectedSort({ + selectedSort: clickedSort, + selectedSortOrder: SelectedSortOrderE.DESC, + }); + } else { + setSelectedSort({ + selectedSort: SelectedSortE.NONE, + selectedSortOrder: SelectedSortOrderE.NOT_SELECTED, + }); + } + }; + + return ( +
+ onClick(SelectedSortE.BY_CREATION_DATE)} + /> + onClick(SelectedSortE.ALPHABETICALLY)} + /> + onClick(SelectedSortE.BY_DUE_DATE)} + /> + onClick(SelectedSortE.STARRED)} + /> +
+ ); +}; + +const SortItem = ({ + text, + isSelected, + selectedOrder, + onClick, +}: { + text: string; + isSelected: boolean; + selectedOrder: SelectedSortOrderE; + onClick: () => void; +}) => { + return ( +
+

{text}

+ {isSelected ? ( + selectedOrder === SelectedSortOrderE.ASC ? ( + + ) : selectedOrder === SelectedSortOrderE.DESC ? ( + + ) : null + ) : null} +
+ ); +}; diff --git a/frontend/src/components/misc/header.tsx b/frontend/src/components/misc/header.tsx new file mode 100644 index 0000000..0863708 --- /dev/null +++ b/frontend/src/components/misc/header.tsx @@ -0,0 +1,59 @@ +import { InfoIcon, SettingsIcon } from "lucide-react"; +import { useContext, useEffect, useRef } from "react"; +import { Link } from "wouter"; +import { MainContext } from "../../provider/mainProvider"; +import { SettingsMenu } from "../menus/settingsMenu"; +import About from "../overlay/aboutOverlay"; + +export default function Header() { + const { showSettings, setShowAbout, setShowSettings } = + useContext(MainContext)!; + const headerRef = useRef(null); + const aboutRef = useRef(null); + const settingsButtonRef = useRef(null); + const aboutButtonRef = useRef(null); + + useEffect(() => { + const header: any = headerRef.current; + const about: any = aboutRef.current; + + if (header && about) { + about.style.top = `${header.height}px`; // Adjust as needed + about.style.height = `calc(100% - ${header.offsetHeight}px)`; + } + }); + + return ( +
+
+ + Logo +

MY TODOS

+ +
+ + +
+
+ + +
+ ); +} diff --git a/frontend/src/components/misc/sidebar.tsx b/frontend/src/components/misc/sidebar.tsx new file mode 100644 index 0000000..50b3582 --- /dev/null +++ b/frontend/src/components/misc/sidebar.tsx @@ -0,0 +1,129 @@ +import { CalendarDays, List, PlusIcon, Trash } from "lucide-react"; +import { RefObject, useContext, useRef, useState } from "react"; +import { Link, useRoute } from "wouter"; +import useDisableSelect from "../../hooks/disableSelect"; +import useShowOnHover from "../../hooks/showOnHover"; +import { DeleteConfirmContext } from "../../provider/deleteConfirmProvider"; +import { MainContext } from "../../provider/mainProvider"; +import { TodoListContext } from "../../provider/todoListProvider"; +import { TaskListTypeI } from "../../types/types"; +import AddTodoList from "../dialogs/todoList/addTodoList"; +import { DeleteTodoListConfirm } from "../dialogs/todoList/deleteTodoListConfirm"; + +export default function Sidebar() { + const [, params] = useRoute("/:listId"); + const listId = params?.listId; + const { showCalendar, changeShowCalendar } = useContext(MainContext)!; + const { taskLists } = useContext(TodoListContext)!; + const [showTaskListAdd, setShowTaskListAdd] = useState(false); + + const map = taskLists.map((e, i) => { + return ; + }); + + return ( +
+

+ Your lists +

+
+
setShowTaskListAdd(true)} + > + +

Add list

+
+
{map}
+
+ {showCalendar ? ( + <> + +

Switch to list

+ + ) : ( + <> + +

+ Switch to calendar +

+ + )} +
+ setShowTaskListAdd(false)} + /> +
+ ); +} + +interface ListItemI { + e: TaskListTypeI; + i: number; + listId?: string; +} + +const ListItem = (props: ListItemI) => { + const { delTaskList } = useContext(TodoListContext)!; + const { isDeleteConfirmation } = useContext(DeleteConfirmContext)!; + const listItemRef: RefObject = useRef(null); + const deleteIconRef: RefObject = useRef(null); + const [showTaskListDelete, setShowTaskListDelete] = useState(false); + useShowOnHover(listItemRef, deleteIconRef); + + const deleteTaskList = (e: any) => { + if (e.shiftKey || isDeleteConfirmation) { + delTaskList(props.e.id); + } else setShowTaskListDelete(true); + }; + + useDisableSelect(listItemRef); + + return ( +
+ +
+

+ {props.e.title} +

+
+ { + deleteTaskList(e); + e.stopPropagation(); + }} + /> +
+
+ + { + setShowTaskListDelete(false); + delTaskList(props.e.id); + }} + open={showTaskListDelete} + close={() => setShowTaskListDelete(false)} + /> +
+ ); +}; diff --git a/frontend/src/components/overlay/aboutOverlay.tsx b/frontend/src/components/overlay/aboutOverlay.tsx new file mode 100644 index 0000000..9145996 --- /dev/null +++ b/frontend/src/components/overlay/aboutOverlay.tsx @@ -0,0 +1,77 @@ +import { GitHub, Group, YouTube } from "@material-ui/icons"; +import { X } from "lucide-react"; +import { forwardRef, RefObject, useContext } from "react"; +import useOutsideClick from "../../hooks/outsideClick"; +import { MainContext } from "../../provider/mainProvider"; + +interface AboutI { + ignoreClick: RefObject[]; +} + +const About = forwardRef((props, ref) => { + const { showAbout, setShowAbout } = useContext(MainContext)!; + + useOutsideClick( + [ref as RefObject, ...props.ignoreClick], + () => setShowAbout(false), + showAbout + ); + + return ( +
+ setShowAbout(false)} + /> +

About

+

+ My Todos is a simple Todo App built using React.js, TailwindCSS, + Vite, LucideIcons and Quarkus as the backend. +

+

+ You can find devonfw here: +

+ +
+ ); +}); + +export default About; diff --git a/frontend/src/components/utils/checkbox.tsx b/frontend/src/components/utils/checkbox.tsx new file mode 100644 index 0000000..f5ba6f0 --- /dev/null +++ b/frontend/src/components/utils/checkbox.tsx @@ -0,0 +1,27 @@ +import { Check } from "lucide-react"; + +interface CheckboxI { + state: boolean; + onClick: () => void; +} + +const Checkbox = (props: CheckboxI) => { + return ( +
props.onClick()} + > + + + +
+ ); +}; + +export default Checkbox; diff --git a/frontend/src/components/CustomLink.tsx b/frontend/src/components/utils/customLink.tsx similarity index 84% rename from frontend/src/components/CustomLink.tsx rename to frontend/src/components/utils/customLink.tsx index 0a31896..aa93695 100644 --- a/frontend/src/components/CustomLink.tsx +++ b/frontend/src/components/utils/customLink.tsx @@ -1,13 +1,13 @@ import React from "react"; import { useRoute, Link } from "wouter"; -interface Props { +interface CustomLinkI { href: string; onClick?: () => void; children: React.ReactNode; } -const CustomLink = (props: Props) => { +const CustomLink = (props: CustomLinkI) => { const [isActive] = useRoute(props.href); const activeObject = { color: "#3f51b5", diff --git a/frontend/src/components/utils/slider.tsx b/frontend/src/components/utils/slider.tsx new file mode 100644 index 0000000..8ee3b4b --- /dev/null +++ b/frontend/src/components/utils/slider.tsx @@ -0,0 +1,23 @@ +interface SliderI { + state: boolean; + onClick: () => void; +} + +const Slider = (props: SliderI) => { + return ( +
props.onClick()} + > + +
+ ); +}; + +export default Slider; diff --git a/frontend/src/context/DeleteConfirmContext.tsx b/frontend/src/context/DeleteConfirmContext.tsx deleted file mode 100644 index 1e4faf4..0000000 --- a/frontend/src/context/DeleteConfirmContext.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { createContext, useState, ReactNode } from "react"; - -interface Props { - children: ReactNode; -} - -interface DeleteConfirmInterface { - isDeleteConfirmation: boolean; - changeDeleteConfirm: () => void; -} - -export const DeleteConfirmContext = createContext( - null -); - -export const DeleteConfirmProvider = ({ children }: Props) => { - const [isDeleteConfirmation, setIsDeleteConfirmation] = useState( - JSON.parse(localStorage.getItem("deleteConfirmation")!) || false - ); - - const changeDeleteConfirm = () => { - localStorage.setItem("deleteConfirmation", String(!isDeleteConfirmation)); - setIsDeleteConfirmation(!isDeleteConfirmation); - }; - - const deleteConfirmValue: DeleteConfirmInterface = { - isDeleteConfirmation, - changeDeleteConfirm, - }; - - return ( - - {children} - - ); -}; diff --git a/frontend/src/context/MainContext.tsx b/frontend/src/context/MainContext.tsx deleted file mode 100644 index 23a3323..0000000 --- a/frontend/src/context/MainContext.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import {TaskItemType} from "../types"; -import {createContext, ReactNode, useEffect, useState} from "react"; -import {useRoute} from "wouter"; - -interface MainContextInterface { - todos: TaskItemType[]; - setTodos: React.Dispatch>; - markComplete: (id: number) => void; - delTodo: (id: number) => void; - deleteAll: () => void; - editTodo: (id: number, text: string) => void; - addTodo: (title: string) => void; - moveTodo: (old: number, new_: number) => void; - markStar: (id: number) => void; -} - -interface Props { - children: ReactNode; -} - -export const MainContext = createContext(null); - -export const MainProvider = ({children}: Props) => { - const [, params] = useRoute("/:listId"); - const listId = +(params?.listId || 1); - const [todos, setTodos] = useState([]); - - useEffect(() => { - fetch(`/api/task/list-with-items/${encodeURIComponent(listId)}`, { - - method: 'GET', - headers: { - 'Content-Type': 'application/json', - } - }) - .then(response => response.json()) - .then(json => setTodos(json.items)) - .catch(error => console.error(error)); - }, []); - - const saveTaskItem = (taskItem: TaskItemType, onSuccess: ((value: number) => any)) => { - // Send data to the backend via POST - fetch('/api/task/item', { - - method: 'POST', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify(taskItem) // body data type must match "Content-Type" header - - }) - .then((response) => response.json()) - .then(onSuccess) - .catch(error => console.error(error)); - } - - const addTodo = (title: string) => { - if (title.trim()) { - const taskItem: TaskItemType = { - id: Number.NaN, - title, - completed: false, - starred: false, - taskListId: listId - } - saveTaskItem(taskItem, (newId) => { - taskItem.id = newId; - const orderTodos = [taskItem, ...todos]; - orderStarAndComplete(orderTodos); - setTodos(orderTodos); - }); - } - }; - const editTodo: (id: number, text: string) => void = ( - id: number, - text: string - ) => { - if (!(text === null) && text.trim()) { - const taskItem = todos.find(todo => todo.id === id); - if (taskItem) { - taskItem.title = text; - saveTaskItem(taskItem, () => setTodos( - todos.map((todo) => { - if (todo.id === id) { - todo = taskItem - } - return todo; - })) - ); - } - } - }; - const markComplete = (id: number) => { - const taskItem = todos.find(todo => todo.id === id); - if (taskItem) { - taskItem.completed = !taskItem.completed; - saveTaskItem(taskItem, () => { - const orderTodos = todos.map(todo => todo.id === id ? taskItem : todo); - orderStarAndComplete(orderTodos); - setTodos(orderTodos); - } - ); - } - }; - - const markStar = (id: number) => { - const taskItem = todos.find(todo => todo.id === id); - if (taskItem) { - taskItem.starred = !taskItem.starred; - saveTaskItem(taskItem, () => { - const orderTodos = todos.map(todo => todo.id === id ? taskItem : todo); - orderStarAndComplete(orderTodos); - setTodos(orderTodos); - } - ); - } - }; - - const orderStarAndComplete = (todos: TaskItemType[]) => { - todos.sort((x, y) => y.starred - x.starred); - todos.sort((x, y) => x.completed - y.completed); - }; - - const delTodo = (id: number) => { - fetch(`/api/task/item/${encodeURIComponent(id)}`, { - method: 'DELETE', - }) - .then(() => setTodos(todos.filter(todo => todo.id !== id))) - .catch(error => console.error(error)); - } - - const deleteAll = () => setTodos([]); - const moveTodo = (old: number, new_: number) => { - const copy = JSON.parse(JSON.stringify(todos)); - const thing = JSON.parse(JSON.stringify(todos[old])); - copy.splice(old, 1); - copy.splice(new_, 0, thing); - setTodos(copy); - }; - - const mainContextValue: MainContextInterface = { - todos, - setTodos, - markComplete, - delTodo, - deleteAll, - editTodo, - addTodo, - moveTodo, - markStar, - }; - - return ( - - {children} - - ); -}; diff --git a/frontend/src/context/SmallTextContext.tsx b/frontend/src/context/SmallTextContext.tsx deleted file mode 100644 index db0a6ff..0000000 --- a/frontend/src/context/SmallTextContext.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { createContext, useState, ReactNode } from "react"; - -interface Props { - children: ReactNode; -} - -interface SmallTextInterface { - isSmallText: boolean; - changeSmallText: () => void; -} - -export const SmallTextContext = createContext(null); - -export const SmallTextProvider = ({ children }: Props) => { - const [isSmallText, setIsSmallText] = useState( - JSON.parse(localStorage.getItem("smallText")!) || false - ); - - const changeSmallText = () => { - window.localStorage.setItem("smallText", String(!isSmallText)); - setIsSmallText(!isSmallText); - }; - - const smallTextValue: SmallTextInterface = { - isSmallText, - changeSmallText, - }; - - return ( - - {children} - - ); -}; diff --git a/frontend/src/context/ThemeContext.tsx b/frontend/src/context/ThemeContext.tsx deleted file mode 100644 index e4a8dfe..0000000 --- a/frontend/src/context/ThemeContext.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { createContext, useEffect, useState, ReactNode } from "react"; -import { - enable as enableDarkMode, - disable as disableDarkMode, -} from "darkreader"; - -interface Props { - children: ReactNode; -} - -interface ThemeInterface { - isDark: boolean; - changeTheme: () => void; -} - -export const ThemeContext = createContext(null); - -export const ThemeProvider = ({ children }: Props) => { - const [isDark, setIsDark] = useState( - JSON.parse(localStorage.getItem("darkTheme")!) || false - ); - - const changeTheme = () => { - setIsDark(!isDark); - if (isDark) { - enableDarkMode({ - brightness: 100, - contrast: 90, - sepia: 10, - }); - } else { - disableDarkMode(); - } - localStorage.setItem("darkTheme", String(isDark)); - }; - - useEffect(() => { - if (isDark) { - enableDarkMode({ - brightness: 100, - contrast: 90, - sepia: 10, - }); - } else disableDarkMode(); - localStorage.setItem("darkTheme", JSON.stringify(isDark)); - }, [isDark]); - - const themeValue: ThemeInterface = { - isDark, - changeTheme, - }; - - return ( - {children} - ); -}; diff --git a/frontend/src/hooks/disableSelect.tsx b/frontend/src/hooks/disableSelect.tsx new file mode 100644 index 0000000..ba288a0 --- /dev/null +++ b/frontend/src/hooks/disableSelect.tsx @@ -0,0 +1,11 @@ +import { RefObject, useEffect } from "react"; + +const useDisableSelect = (refToDisableSelect: RefObject) => { + useEffect(() => { + refToDisableSelect.current!.onselectstart = () => { + return false; + }; + }, [refToDisableSelect]); +}; + +export default useDisableSelect; diff --git a/frontend/src/hooks/hasOverflow.tsx b/frontend/src/hooks/hasOverflow.tsx new file mode 100644 index 0000000..8d3267f --- /dev/null +++ b/frontend/src/hooks/hasOverflow.tsx @@ -0,0 +1,34 @@ +import { RefObject, useEffect, useState } from "react"; + +const useHasOverflow = (ref: RefObject) => { + const [hasOverflow, setHasOverflow] = useState(false); + + useEffect(() => { + const checkOverflow = () => { + if (ref.current) { + setHasOverflow( + ref.current.scrollHeight > ref.current.clientHeight || + ref.current.scrollWidth > ref.current.clientWidth + ); + } + }; + + const observer = new MutationObserver(checkOverflow); + + if (ref.current) { + observer.observe(ref.current, { childList: true, subtree: true }); + } + + checkOverflow(); + window.addEventListener("resize", checkOverflow); + + return () => { + observer.disconnect(); + window.removeEventListener("resize", checkOverflow); + }; + }, [ref]); + + return hasOverflow; +}; + +export default useHasOverflow; diff --git a/frontend/src/hooks/outsideClick.tsx b/frontend/src/hooks/outsideClick.tsx new file mode 100644 index 0000000..68195f3 --- /dev/null +++ b/frontend/src/hooks/outsideClick.tsx @@ -0,0 +1,28 @@ +import { RefObject, useEffect } from "react"; + +const useOutsideClick = ( + refs: RefObject[], + callback: () => void, + enabled: boolean = true +) => { + useEffect(() => { + if (!enabled) return; + + const handleClickOutside = (event: MouseEvent) => { + for (const ref of refs) { + if (ref.current && ref.current.contains(event.target as Node)) { + return; + } + } + + callback(); + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [refs, callback, enabled]); +}; + +export default useOutsideClick; diff --git a/frontend/src/hooks/scrollBarWidth.tsx b/frontend/src/hooks/scrollBarWidth.tsx new file mode 100644 index 0000000..dd56a71 --- /dev/null +++ b/frontend/src/hooks/scrollBarWidth.tsx @@ -0,0 +1,26 @@ +import { useEffect, useState } from "react"; + +const useScrollbarWidth = () => { + const [scrollbarWidth, setScrollbarWidth] = useState(0); + + useEffect(() => { + const getScrollbarWidth = () => { + const div = document.createElement("div"); + div.style.position = "absolute"; + div.style.top = "-9999px"; + div.style.width = "100px"; + div.style.height = "100px"; + div.style.overflow = "scroll"; + document.body.appendChild(div); + const scrollbarWidth = div.offsetWidth - div.clientWidth; + document.body.removeChild(div); + return scrollbarWidth; + }; + + setScrollbarWidth(getScrollbarWidth()); + }, []); + + return scrollbarWidth; +}; + +export default useScrollbarWidth; diff --git a/frontend/src/hooks/showOnHover.tsx b/frontend/src/hooks/showOnHover.tsx new file mode 100644 index 0000000..a674d1f --- /dev/null +++ b/frontend/src/hooks/showOnHover.tsx @@ -0,0 +1,22 @@ +import { RefObject, useEffect } from "react"; + +const useShowOnHover = ( + refToObserve: RefObject, + refToShow: RefObject +) => { + useEffect(() => { + if (null != refToObserve.current) { + refToObserve.current!.addEventListener("mouseover", () => { + refToShow.current!.classList.add("flex"); + refToShow.current!.classList.remove("hidden"); + }); + + refToObserve.current!.addEventListener("mouseout", () => { + refToShow.current!.classList.remove("flex"); + refToShow.current!.classList.add("hidden"); + }); + } + }, [refToObserve, refToShow]); +}; + +export default useShowOnHover; diff --git a/frontend/src/hooks/useChangeMenuIcon.tsx b/frontend/src/hooks/useChangeMenuIcon.tsx deleted file mode 100644 index edac502..0000000 --- a/frontend/src/hooks/useChangeMenuIcon.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { useMediaQuery } from "@material-ui/core"; -import { MoreVert, MoreHoriz, SvgIconComponent } from "@material-ui/icons"; - -export default function useChangeMenuIcon(): SvgIconComponent { - const Icon: SvgIconComponent = () => - useMediaQuery("(max-width: 768px)") ? : ; - return Icon; -} diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 3ee4542..b32dac4 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -1,19 +1,22 @@ import { render } from "react-dom"; -import { MainProvider } from "./context/MainContext"; -import { ThemeProvider } from "./context/ThemeContext"; -import { DeleteConfirmProvider } from "./context/DeleteConfirmContext"; -import { SmallTextProvider } from "./context/SmallTextContext"; -import App from "./App"; -import "./styles.css"; -import * as serviceWorkerRegistration from "./serviceWorkerRegistration"; +import App from "./app"; +import { DeleteConfirmProvider } from "./provider/deleteConfirmProvider"; +import { MainProvider } from "./provider/mainProvider"; +import { ThemeProvider } from "./provider/themeProvider"; +import { TodoListProvider } from "./provider/todoListProvider"; +import { TodoProvider } from "./provider/todoProvider"; +import "./styles/styles.css"; +import * as serviceWorkerRegistration from "./worker/serviceWorkerRegistration"; render( - + + - + + , diff --git a/frontend/src/pages/About.tsx b/frontend/src/pages/About.tsx deleted file mode 100644 index 02a52b6..0000000 --- a/frontend/src/pages/About.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { Container } from "@material-ui/core"; -import { - GitHub, - YouTube, - Group -} from "@material-ui/icons"; - -const About = () => { - return ( - <> - -

- My Todos is a simple Todo App built using React.js and styled using Material UI. -

-

devonfw

- - - - - - - - - -
- - ); -}; - -export default About; diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx deleted file mode 100644 index b6cd0a5..0000000 --- a/frontend/src/pages/Settings.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { Container, Switch, useMediaQuery } from "@material-ui/core"; -import { useContext } from "react"; -import { ThemeContext } from "../context/ThemeContext"; -import { DeleteConfirmContext } from "../context/DeleteConfirmContext"; -import { SmallTextContext } from "../context/SmallTextContext"; - -const Settings = () => { - const { isDeleteConfirmation, changeDeleteConfirm } = useContext( - DeleteConfirmContext - )!; - const { isDark, changeTheme } = useContext(ThemeContext)!; - const { isSmallText, changeSmallText } = useContext(SmallTextContext)!; - const matches = useMediaQuery("(max-width: 768px)"); - - return ( - <> - -

- Dark Mode: - -

-

- Small Text Mode: - -

-

- Disable Delete Confirmation: - -

-
- - ); -}; - -export default Settings; diff --git a/frontend/src/provider/deleteConfirmProvider.tsx b/frontend/src/provider/deleteConfirmProvider.tsx new file mode 100644 index 0000000..ab0af56 --- /dev/null +++ b/frontend/src/provider/deleteConfirmProvider.tsx @@ -0,0 +1,22 @@ +import { createContext, useState } from "react"; + +export const DeleteConfirmContext = createContext(null); + +export const DeleteConfirmProvider = ({ children }: PropsI) => { + const [isDeleteConfirmation, setIsDeleteConfirmation] = useState( + JSON.parse(localStorage.getItem("deleteConfirmation")!) || false + ); + + const changeDeleteConfirm = () => { + localStorage.setItem("deleteConfirmation", String(!isDeleteConfirmation)); + setIsDeleteConfirmation(!isDeleteConfirmation); + }; + + return ( + + {children} + + ); +}; diff --git a/frontend/src/provider/mainProvider.tsx b/frontend/src/provider/mainProvider.tsx new file mode 100644 index 0000000..f18fc9a --- /dev/null +++ b/frontend/src/provider/mainProvider.tsx @@ -0,0 +1,35 @@ +import { createContext, useState } from "react"; + +export const MainContext = createContext(null); + +export const MainProvider = ({ children }: PropsI) => { + const [errorAlert, setErrorAlert] = useState(""); + const [successAlert, setSuccessAlert] = useState(""); + + const [showSettings, setShowSettings] = useState(false); + const [showAbout, setShowAbout] = useState(false); + const [showCalendar, setShowCalendar] = useState(false); + + const changeShowCalendar = () => { + setShowCalendar(!showCalendar); + }; + + return ( + + {children} + + ); +}; diff --git a/frontend/src/provider/themeProvider.tsx b/frontend/src/provider/themeProvider.tsx new file mode 100644 index 0000000..084b484 --- /dev/null +++ b/frontend/src/provider/themeProvider.tsx @@ -0,0 +1,28 @@ +import { createContext, useEffect, useState } from "react"; + +export const ThemeContext = createContext(null); + +export const ThemeProvider = ({ children }: PropsI) => { + const [isDark, setIsDark] = useState( + JSON.parse(localStorage.getItem("darkTheme")!) || false + ); + + const changeTheme = () => { + setIsDark(!isDark); + }; + + useEffect(() => { + if (isDark) { + document.documentElement.classList.add("dark"); + } else { + document.documentElement.classList.remove("dark"); + } + localStorage.setItem("darkTheme", String(isDark)); + }, [isDark]); + + return ( + + {children} + + ); +}; diff --git a/frontend/src/provider/todoListProvider.tsx b/frontend/src/provider/todoListProvider.tsx new file mode 100644 index 0000000..5998ef8 --- /dev/null +++ b/frontend/src/provider/todoListProvider.tsx @@ -0,0 +1,155 @@ +import { createContext, useContext, useEffect, useState } from "react"; +import { useRoute } from "wouter"; +import { navigate } from "wouter/use-location"; +import { TaskListTypeI } from "../types/types"; +import { MainContext } from "./mainProvider"; + +export const TodoListContext = createContext(null); + +export const TodoListProvider = ({ children }: PropsI) => { + const { setErrorAlert, setSuccessAlert } = useContext(MainContext)!; + + const [, params] = useRoute("/:listId"); + const listId = params?.listId; + const [taskLists, setTaskLists] = useState([]); + + useEffect(() => { + fetch(`/api/task/lists`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => response.json()) + .then((json) => setTaskLists(json)) + .catch((error) => { + console.error(error); + setErrorAlert("List could not be loaded!"); + }); + }, [setErrorAlert]); + + function loadLists() { + fetch(`/api/task/lists`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => response.json()) + .then((json) => setTaskLists(json)) + .catch((error) => { + console.error(error); + setErrorAlert("List could not be loaded!"); + }); + } + + function generateRandomList(title: string) { + fetch(`/api/task/list/multiple-random-activities`, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "text/plain", + }, + body: title, // body data type must match "Content-Type" header + }) + .then((response) => loadLists()) + .catch((error) => { + console.error(error); + setErrorAlert("List could not be loaded!"); + }); + } + + function editTodoList(newTitle: string) { + if (undefined === listId) { + return; + } + + const taskList = taskLists.find((taskList) => taskList.id === +listId!); + + if (taskList) { + taskList.title = newTitle; + + fetch("/api/task/list", { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify(taskList), + }) + .then((response) => response.json()) + .then((newVersion) => { + taskList.version = newVersion; + setTaskLists(taskLists); + setSuccessAlert("Todo List edit!"); + }) + .catch((error) => { + console.error(error); + setErrorAlert("Todo List could not be saved!"); + }); + } + } + + const addTaskList = (title: string) => { + if (title.trim()) { + const taskList: TaskListTypeI = { + id: Number.NaN, + version: 0, + title, + }; + + fetch("/api/task/list", { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify(taskList), // body data type must match "Content-Type" header + }) + .then((response) => response.json()) + .then((newId) => { + taskList.id = newId; + const newTaskLists = [taskList, ...taskLists]; + setTaskLists(newTaskLists); + setSuccessAlert("Todo List created!"); + }) + .catch((error) => { + console.error(error); + setErrorAlert("List could not be created!"); + }); + } + }; + + const delTaskList = (id: number) => { + fetch(`/api/task/list/${encodeURIComponent(id)}`, { + method: "DELETE", + }) + .then(() => { + setTaskLists(taskLists.filter((taskList) => taskList.id !== id)); + if (undefined !== listId && id === +listId) { + navigate("/"); + } + setSuccessAlert("Todo List deleted!"); + }) + .catch((error) => { + console.error(error); + setErrorAlert("List could not be deleted!"); + }); + }; + + return ( + + {children} + + ); +}; diff --git a/frontend/src/provider/todoProvider.tsx b/frontend/src/provider/todoProvider.tsx new file mode 100644 index 0000000..d35b0af --- /dev/null +++ b/frontend/src/provider/todoProvider.tsx @@ -0,0 +1,313 @@ +import { createContext, useContext, useEffect, useState } from "react"; +import { useRoute } from "wouter"; +import { SelectedFilterE } from "../components/menus/filterMenu"; +import { + SelectedSortE, + SelectedSortOrderE, +} from "../components/menus/sortMenu"; +import { TaskItemTypeI } from "../types/types"; +import { MainContext } from "./mainProvider"; + +export const TodoContext = createContext(null); + +export const TodoProvider = ({ children }: PropsI) => { + const { setErrorAlert, setSuccessAlert } = useContext(MainContext)!; + + const [, params] = useRoute("/:listId"); + const listId = params?.listId; + const [todos, setTodos] = useState([]); + + useEffect(() => { + if (undefined === listId) { + setTodos([]); + return; + } + + fetch(`/api/task/list-with-items/${encodeURIComponent(+listId!)}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => response.json()) + .then((json) => setTodos(json.items)) + .catch((error) => { + console.error(error); + setErrorAlert("Items could not be loaded!"); + }); + }, [listId, setErrorAlert]); + + const loadItems = () => { + fetch(`/api/task/list-with-items/${encodeURIComponent(+listId!)}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => response.json()) + .then((json) => setTodos(json.items)) + .catch((error) => { + console.error(error); + setErrorAlert("Items could not be loaded!"); + }); + }; + + const saveTaskItem = ( + taskItem: TaskItemTypeI, + onSuccess: (value: number) => any + ) => { + // Send data to the backend via POST + fetch("/api/task/item", { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify(taskItem), // body data type must match "Content-Type" header + }) + .then((response) => response.json()) + .then(onSuccess) + .catch((error) => { + console.error(error); + setErrorAlert("Item could not be saved!"); + }); + }; + + const addRandomTodo = () => { + if (undefined === listId) { + return; + } + + // Send data to the backend via POST + fetch(`/api/task/list/${encodeURIComponent(+listId!)}/random-activity`, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + }) + .then((response) => { + loadItems(); + }) + .catch((error) => { + console.error(error); + setErrorAlert("Item could not be saved!"); + }); + }; + + const addTodo = (title: string, deadline: string | null) => { + if (undefined === listId) { + return; + } + + if (title.trim()) { + const taskItem: TaskItemTypeI = { + id: Number.NaN, + title, + version: 0, + completed: false, + starred: false, + taskListId: +listId!, + deadline: 0 === (deadline?.length ?? 0) ? null : deadline, + }; + + saveTaskItem(taskItem, (newId) => { + taskItem.id = newId; + const orderTodos = [taskItem, ...todos]; + setTodos(orderTodos); + setSuccessAlert("Item created!"); + }); + } + }; + + const editTodo: ( + id: number, + text: string, + deadline: string | null + ) => void = (id: number, text: string, deadline: string | null) => { + if (!(text === null) && text.trim()) { + const taskItem = todos.find((todo) => todo.id === id); + if (taskItem) { + taskItem.title = text; + taskItem.deadline = deadline; + saveTaskItem(taskItem, (newVersion) => { + taskItem.version = newVersion; + setTodos( + todos.map((todo) => { + if (todo.id === id) { + todo = taskItem; + } + return todo; + }) + ); + setSuccessAlert("Item edited!"); + }); + } + } + }; + + const markComplete = (id: number) => { + const taskItem = todos.find((todo) => todo.id === id); + if (taskItem) { + taskItem.completed = !taskItem.completed; + saveTaskItem(taskItem, (newVersion) => { + taskItem.version = newVersion; + const orderTodos = todos.map((todo) => + todo.id === id ? taskItem : todo + ); + setTodos(orderTodos); + setSuccessAlert("Item marked as completed!"); + }); + } + }; + + const markStar = (id: number) => { + const taskItem = todos.find((todo) => todo.id === id); + if (taskItem) { + taskItem.starred = !taskItem.starred; + saveTaskItem(taskItem, (newVersion) => { + taskItem.version = newVersion; + const orderTodos = todos.map((todo) => + todo.id === id ? taskItem : todo + ); + setTodos(orderTodos); + setSuccessAlert(`Item ${taskItem.starred ? "" : "un"}starred!`); + }); + } + }; + + const delTodo = (id: number) => { + fetch(`/api/task/item/${encodeURIComponent(id)}`, { + method: "DELETE", + }) + .then(() => { + setTodos(todos.filter((todo) => todo.id !== id)); + setSuccessAlert("Item deleted!"); + }) + .catch((error) => { + console.error(error); + setErrorAlert("Item could not be deleted!"); + }); + }; + + const deleteAll = () => setTodos([]); + + const moveTodo = (old: number, new_: number) => { + const copy = JSON.parse(JSON.stringify(todos)); + const thing = JSON.parse(JSON.stringify(todos[old])); + copy.splice(old, 1); + copy.splice(new_, 0, thing); + setTodos(copy); + }; + + const applyFilter = (todos: TaskItemTypeI[], filter: SelectedFilterE) => { + const sortedTodos = [...todos]; + + switch (filter) { + case SelectedFilterE.NONE: + return sortedTodos; + case SelectedFilterE.WITH_DUE_DATE: + return sortedTodos.filter((e) => e.deadline != null); + case SelectedFilterE.WITHOUT_DUE_DATE: + return sortedTodos.filter((e) => e.deadline == null); + case SelectedFilterE.ONLY_STARRED: + return sortedTodos.filter((e) => e.starred); + case SelectedFilterE.NOT_STARRED: + return sortedTodos.filter((e) => !e.starred); + } + }; + + const applySort = ( + todos: TaskItemTypeI[], + sort: { + selectedSort: SelectedSortE; + selectedSortOrder: SelectedSortOrderE; + } + ) => { + const sortedTodos = [...todos]; + + if ( + sort.selectedSort === SelectedSortE.NONE || + sort.selectedSortOrder === SelectedSortOrderE.NOT_SELECTED + ) { + return todos; + } + + switch (sort.selectedSort) { + case SelectedSortE.BY_CREATION_DATE: + if (sort.selectedSortOrder === SelectedSortOrderE.ASC) { + return sortedTodos.sort((a, b) => a.id - b.id); + } else { + return sortedTodos.sort((a, b) => b.id - a.id); + } + case SelectedSortE.ALPHABETICALLY: + if (sort.selectedSortOrder === SelectedSortOrderE.ASC) { + return sortedTodos.sort((a, b) => a.title.localeCompare(b.title)); + } else { + return sortedTodos.sort((a, b) => b.title.localeCompare(a.title)); + } + case SelectedSortE.BY_DUE_DATE: + const sortedDateTodos = []; + if (sort.selectedSortOrder === SelectedSortOrderE.ASC) { + sortedDateTodos.push( + ...sortedTodos + .filter((e) => null != e.deadline) + .sort((a, b) => { + console.log(a); + console.log(b); + + return ( + new Date(a.deadline!).getTime() - + new Date(b.deadline!).getTime() + ); + }) + ); + } else { + sortedDateTodos.push( + ...sortedTodos + .filter((e) => null != e.deadline) + .sort((a, b) => { + return ( + new Date(b.deadline!).getTime() - + new Date(a.deadline!).getTime() + ); + }) + ); + } + sortedDateTodos.push(...sortedTodos.filter((e) => null == e.deadline)); + return sortedDateTodos; + case SelectedSortE.STARRED: + if (sort.selectedSortOrder === SelectedSortOrderE.ASC) { + return sortedTodos.sort( + (a, b) => Number(b.starred) - Number(a.starred) + ); + } else { + return sortedTodos.sort( + (a, b) => Number(a.starred) - Number(b.starred) + ); + } + } + }; + + return ( + + {children} + + ); +}; diff --git a/frontend/src/react-app-env.d.ts b/frontend/src/react-app-env.d.ts deleted file mode 100644 index 6431bc5..0000000 --- a/frontend/src/react-app-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/frontend/src/setupProxy.js b/frontend/src/setupProxy.js deleted file mode 100644 index d807abd..0000000 --- a/frontend/src/setupProxy.js +++ /dev/null @@ -1,16 +0,0 @@ -const { createProxyMiddleware } = require('http-proxy-middleware'); - -const proxy = { - target: 'http://localhost:8080', - changeOrigin: true, - pathRewrite: { - '^/api/': '/' // remove base path - } -}; - -module.exports = function(app) { - app.use( - '/api', - createProxyMiddleware(proxy) - ); -}; \ No newline at end of file diff --git a/frontend/src/styles.css b/frontend/src/styles.css deleted file mode 100644 index cb55f4a..0000000 --- a/frontend/src/styles.css +++ /dev/null @@ -1,19 +0,0 @@ -@media screen and (max-width: 768px) { - .add-todo { - margin-top: 10px; - } - .todo-text { - font-size: 1.3rem !important; - } - .card-content { - padding: 12px !important; - } -} - -.card-content { - padding: 16px; -} -a { - text-decoration: none; - color: inherit; -} diff --git a/frontend/src/styles/calendar.css b/frontend/src/styles/calendar.css new file mode 100644 index 0000000..c1da407 --- /dev/null +++ b/frontend/src/styles/calendar.css @@ -0,0 +1,120 @@ +.dark .rbc-month-view { + border-color: var(--color-black); +} + +.dark .rbc-header { + border-color: var(--color-black); + color: white +} + +.dark .rbc-month-row { + border-color: var(--color-black); +} + +.dark .rbc-day-bg { + background-color: var(--color-light-black); + border-color: var(--color-black); +} + +.dark .rbc-time-view { + border-color: var(--color-black); +} + +.dark .rbc-time-header-content { + border-color: var(--color-black); +} + +.dark .rbc-time-content { + border-color: var(--color-black); +} + +.dark .rbc-timeslot-group { + border-color: var(--color-black); +} + +.dark .rbc-time-slot { + border-color: var(--color-black); +} + +.dark .rbc-events-container { + border-color: var(--color-black); +} + +.dark .rbc-label { + color: white !important +} + +.dark .rbc-date-cell { + color: white; +} + +.dark .rbc-off-range-bg { + background-color: var(--color-black); +} + +.dark .rbc-off-range { + color: gray; +} + +.dark .rbc-toolbar-label { + color: white; +} + +.dark .rbc-btn-group button { + border-color: var(--color-primary); + color: white; +} + +.dark .rbc-toolbar button:hover { + background-color: var(--color-light-primary); + color: var(--color-black); +} + +.dark .rbc-toolbar button:focus { + background-color: var(--color-light-primary); + color: var(--color-black); +} + +.dark button.rbc-active { + background-color: var(--color-light-primary); + color: var(--color-black); +} + +.dark .rbc-agenda-view { + color: white +} + +.rbc-day-bg.rbc-today { + background-color: var(--color-primary); +} + +.rbc-header.rbc-today { + color: white; + background-color: var(--color-primary); +} + +.rbc-now { + color: white +} + +.rbc-day-slot.rbc-today { + background-color: var(--color-light-primary); +} + +.rbc-day-slot.rbc-today .rbc-current-time-indicator { + background-color: var(--color-primary); + height: 2px +} + +.rbc-time-view { + flex-basis: 0; +} + +.rbc-event { + background-color: var(--color-light-primary) !important; +} + +.rbc-show-more { + background-color: var(--color-light-primary); + color: white; +} \ No newline at end of file diff --git a/frontend/src/styles/styles.css b/frontend/src/styles/styles.css new file mode 100644 index 0000000..d3dd40f --- /dev/null +++ b/frontend/src/styles/styles.css @@ -0,0 +1,43 @@ +@import "tailwindcss"; +@import "react-big-calendar/lib/css/react-big-calendar.css"; +@import "./calendar.css"; + +@custom-variant dark (&:where(.dark, .dark *)); + +@theme { + --color-primary: #3F51B5; + --color-light-primary: #a2b0ff; + --color-light-gray: #EDEDED; + --color-light-gray2: #D9D9D9; + --color-light-black: #2c2c2c; + --color-black: #1e1e1e; + --color-black-alpha-95: #1e1e1eF3; + --color-red: #C72424; + --color-green: #24C73F; +} + +* { + scrollbar-color: var(--color-light-gray-2) var(--color-light-gray); + scrollbar-width: thin; + scroll-padding: 20px; +} + +@media screen and (max-width: 768px) { + .add-todo { + margin-top: 10px; + } + .todo-text { + font-size: 1.3rem !important; + } + .card-content { + padding: 12px !important; + } +} + +.card-content { + padding: 16px; +} +a { + text-decoration: none; + color: inherit; +} diff --git a/frontend/src/types.d.ts b/frontend/src/types.d.ts deleted file mode 100644 index 0306351..0000000 --- a/frontend/src/types.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface TaskItemType { - id: number; - title: string; - completed: any; - starred: any; - taskListId: number; -} diff --git a/frontend/src/types/deleteConfirmProvider.d.ts b/frontend/src/types/deleteConfirmProvider.d.ts new file mode 100644 index 0000000..0bfe373 --- /dev/null +++ b/frontend/src/types/deleteConfirmProvider.d.ts @@ -0,0 +1,4 @@ +interface DeleteConfirmI { + isDeleteConfirmation: boolean; + changeDeleteConfirm: () => void; +} diff --git a/frontend/src/types/mainProvider.d.ts b/frontend/src/types/mainProvider.d.ts new file mode 100644 index 0000000..639eeb1 --- /dev/null +++ b/frontend/src/types/mainProvider.d.ts @@ -0,0 +1,12 @@ +interface MainContextI { + errorAlert: string; + successAlert: string; + showSettings: boolean; + showAbout: boolean; + showCalendar: boolean; + setErrorAlert: React.Dispatch>; + setSuccessAlert: React.Dispatch>; + setShowSettings: React.Dispatch>; + setShowAbout: React.Dispatch>; + changeShowCalendar: () => void; +} diff --git a/frontend/src/types/props.d.ts b/frontend/src/types/props.d.ts new file mode 100644 index 0000000..b72e89f --- /dev/null +++ b/frontend/src/types/props.d.ts @@ -0,0 +1,3 @@ +interface PropsI { + children: ReactNode; +} diff --git a/frontend/src/types/themeProvider.d.ts b/frontend/src/types/themeProvider.d.ts new file mode 100644 index 0000000..b1966f5 --- /dev/null +++ b/frontend/src/types/themeProvider.d.ts @@ -0,0 +1,4 @@ +interface ThemeI { + isDark: boolean; + changeTheme: () => void; +} diff --git a/frontend/src/types/todoListProvider.d.ts b/frontend/src/types/todoListProvider.d.ts new file mode 100644 index 0000000..cac3066 --- /dev/null +++ b/frontend/src/types/todoListProvider.d.ts @@ -0,0 +1,9 @@ +interface TodoListInterfaceI { + taskLists: TaskListType[]; + + setTaskLists: React.Dispatch>; + editTodoList: (newTitle: string) => void; + addTaskList: (title: string) => void; + delTaskList: (id: number) => void; + generateRandomList: (title: string) => void; +} diff --git a/frontend/src/types/todoProvider.d.ts b/frontend/src/types/todoProvider.d.ts new file mode 100644 index 0000000..86679de --- /dev/null +++ b/frontend/src/types/todoProvider.d.ts @@ -0,0 +1,24 @@ +interface TodoInterfaceI { + todos: TaskItemType[]; + + setTodos: React.Dispatch>; + markComplete: (id: number) => void; + delTodo: (id: number) => void; + deleteAll: () => void; + editTodo: (id: number, text: string, deadline: string | null) => void; + addTodo: (title: string, deadline: string | null) => void; + addRandomTodo: () => void; + moveTodo: (old: number, new_: number) => void; + markStar: (id: number) => void; + applyFilter: ( + todos: TaskItemTypeI[], + filter: SelectedFilterE + ) => TaskItemTypeI[]; + applySort: ( + todos: TaskItemTypeI[], + sort: { + selectedSort: SelectedSortE; + selectedSortOrder: SelectedSortOrderE; + } + ) => TaskItemTypeI[]; +} diff --git a/frontend/src/types/types.d.ts b/frontend/src/types/types.d.ts new file mode 100644 index 0000000..bf582f7 --- /dev/null +++ b/frontend/src/types/types.d.ts @@ -0,0 +1,15 @@ +export interface TaskItemTypeI { + id: number; + title: string; + version: number; + completed: any; + starred: any; + taskListId: number; + deadline: string | null; +} + +export interface TaskListTypeI { + id: number; + version: number; + title: string; +} diff --git a/frontend/src/service-worker.ts b/frontend/src/worker/service-worker.ts similarity index 100% rename from frontend/src/service-worker.ts rename to frontend/src/worker/service-worker.ts diff --git a/frontend/src/serviceWorkerRegistration.ts b/frontend/src/worker/serviceWorkerRegistration.ts similarity index 98% rename from frontend/src/serviceWorkerRegistration.ts rename to frontend/src/worker/serviceWorkerRegistration.ts index 6d59991..a053855 100644 --- a/frontend/src/serviceWorkerRegistration.ts +++ b/frontend/src/worker/serviceWorkerRegistration.ts @@ -28,7 +28,7 @@ type Config = { export function register(config?: Config) { if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) { // The URL constructor is available in all browsers that support SW. - const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); + const publicUrl = new URL(process.env.PUBLIC_URL!, window.location.href); if (publicUrl.origin !== window.location.origin) { // Our service worker won't work if PUBLIC_URL is on a different origin // from what our page is served on. This might happen if a CDN is used to diff --git a/frontend/vite.config.mjs b/frontend/vite.config.mjs new file mode 100644 index 0000000..235ac62 --- /dev/null +++ b/frontend/vite.config.mjs @@ -0,0 +1,21 @@ +import tailwindcss from '@tailwindcss/vite'; +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +export default defineConfig(() => { + return { + build: { + outDir: 'build', + }, + plugins: [react(), tailwindcss(),], + server: { + proxy: { + '/api': { + target: 'http://localhost:8080', + changeOrigin: true, + rewrite: (path) => path.replace("/api", ""), + } + } + } + }; +}); \ No newline at end of file