diff --git a/.env b/.env new file mode 100644 index 00000000..f95c81f1 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +VITE_APP_BACKEND_URL=http://localhost:5000 \ No newline at end of file diff --git a/.gitignore b/.gitignore index d7de12f3..a547bf36 100644 --- a/.gitignore +++ b/.gitignore @@ -12,8 +12,6 @@ dist dist-ssr *.local -.env - # Editor directories and files .vscode/* !.vscode/extensions.json diff --git a/package-lock.json b/package-lock.json index fb50dd28..b8f8182f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "npm-task", "version": "0.0.0", "dependencies": { - "axios": "^1.7.7", + "axios": "^1.7.9", "react": "^18.3.1", "react-dom": "^18.3.1" }, @@ -1887,9 +1887,9 @@ } }, "node_modules/axios": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", - "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", diff --git a/package.json b/package.json index 0b121eef..e4727ed7 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "test": "vitest" }, "dependencies": { - "axios": "^1.7.7", + "axios": "^1.7.9", "react": "^18.3.1", "react-dom": "^18.3.1" }, diff --git a/src/App.jsx b/src/App.jsx index 899cfa00..1e1e998f 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,27 +1,94 @@ import TaskList from './components/TaskList.jsx'; +import NewTaskForm from './components/NewTaskForm.jsx'; import './App.css'; - -const TASKS = [ - { - id: 1, - title: 'Mow the lawn', - isComplete: false, - }, - { - id: 2, - title: 'Cook Pasta', - isComplete: true, - }, -]; +import { useState, useEffect } from 'react'; +import axios from 'axios'; const App = () => { + const [tasks, setTasks] = useState([]); + const API_BASE_URL = import.meta.env.VITE_APP_BACKEND_URL; + + useEffect(() => { + axios + .get(`${API_BASE_URL}/tasks`) + .then((response) => { + const formattedTasks = response.data.map((task) => ({ + id: task.id, + title: task.title, + isComplete: task.is_complete, + })); + setTasks(formattedTasks); + }) + .catch((error) => { + console.error('Error fetching tasks:', error); + }); + }, []); + + const toggleCompleteTask = (taskId, isComplete) => { + const endpoint = isComplete + ? `${API_BASE_URL}/tasks/${taskId}/mark_incomplete` + : `${API_BASE_URL}/tasks/${taskId}/mark_complete`; + + axios + .patch(endpoint) + .then(() => { + setTasks((prevTasks) => + prevTasks.map((task) => + task.id === taskId ? { ...task, isComplete: !isComplete } : task + ) + ); + }) + .catch((error) => { + console.error('Error toggling task completion:', error); + }); + }; + + const deleteTask = (taskId) => { + axios + .delete(`${API_BASE_URL}/tasks/${taskId}`) + .then(() => { + setTasks((prevTasks) => prevTasks.filter((task) => task.id !== taskId)); + }) + .catch((error) => { + console.error('Error deleting task:', error); + }); + }; + + const convertFromApi = (apiTask) => { + const newTask = { + ...apiTask, + isComplete: apiTask.is_complete, + }; + delete newTask.is_complete; + return newTask; + }; + + const handleSubmit = (taskData) => { + axios.post(`${API_BASE_URL}/tasks`, taskData) + .then((result) => { + setTasks((prevTasks) => [convertFromApi(result.data.task), ...prevTasks]); + }) + .catch((error) => console.log(error)); + }; + return (

Ada's Task List

-
{}
+
+ +
+
+ +
); diff --git a/src/components/NewTaskForm.jsx b/src/components/NewTaskForm.jsx new file mode 100644 index 00000000..0a9860e8 --- /dev/null +++ b/src/components/NewTaskForm.jsx @@ -0,0 +1,47 @@ +import { useState } from 'react'; +import PropTypes from 'prop-types'; + +const NewTaskForm = ({ handleSubmit }) => { + const kDefaultFormState = { + title: '', + description: '', + isComplete: false, + }; + + const [formData, setFormData] = useState(kDefaultFormState); + + const handleChange = event => { + const fieldName = event.target.name; + const fieldValue = event.target.value; + const newFormData = {...formData, [fieldName]: fieldValue}; + setFormData(newFormData); + }; + + const onHandleSubmit = (event) => { + event.preventDefault(); + handleSubmit(formData); + setFormData(kDefaultFormState); + }; + + return ( +
+
+ + +
+
+ + +
+
+ +
+
+ ); +}; + +NewTaskForm.propTypes = { + handleSubmit: PropTypes.func.isRequired, +}; + +export default NewTaskForm; \ No newline at end of file diff --git a/src/components/Task.jsx b/src/components/Task.jsx index a687076c..7887f8ac 100644 --- a/src/components/Task.jsx +++ b/src/components/Task.jsx @@ -1,21 +1,23 @@ -import { useState } from 'react'; import PropTypes from 'prop-types'; - import './Task.css'; -const Task = ({ id, title, isComplete }) => { - const [complete, setComplete] = useState(isComplete); - const buttonClass = complete ? 'tasks__item__toggle--completed' : ''; - +const Task = ({ id, title, isComplete, onToggleComplete, onDeleteTask }) => { return (
  • - +
  • ); }; @@ -24,6 +26,8 @@ Task.propTypes = { id: PropTypes.number.isRequired, title: PropTypes.string.isRequired, isComplete: PropTypes.bool.isRequired, + onToggleComplete: PropTypes.func.isRequired, + onDeleteTask: PropTypes.func.isRequired, }; export default Task; diff --git a/src/components/TaskList.jsx b/src/components/TaskList.jsx index 199ef575..e947597c 100644 --- a/src/components/TaskList.jsx +++ b/src/components/TaskList.jsx @@ -1,8 +1,8 @@ import PropTypes from 'prop-types'; -import Task from './Task.jsx'; +import Task from './Task'; // Import Task component import './TaskList.css'; -const TaskList = ({ tasks }) => { +const TaskList = ({ tasks, onToggleComplete, onDeleteTask }) => { const getTaskListJSX = (tasks) => { return tasks.map((task) => { return ( @@ -11,10 +11,13 @@ const TaskList = ({ tasks }) => { id={task.id} title={task.title} isComplete={task.isComplete} + onToggleComplete={onToggleComplete} + onDeleteTask={onDeleteTask} /> ); }); }; + return ; }; @@ -26,6 +29,8 @@ TaskList.propTypes = { isComplete: PropTypes.bool.isRequired, }) ).isRequired, + onToggleComplete: PropTypes.func.isRequired, + onDeleteTask: PropTypes.func.isRequired, }; export default TaskList;