Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Digital/checkpoint/wave 04 async vite updates #13

Open
wants to merge 3 commits into
base: digital/checkpoint/wave-03-async-vite
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 55 additions & 22 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import { useState, useEffect } from 'react';
import TaskList from './components/TaskList.jsx';
import './App.css';
import axios from 'axios';
import NewTaskForm from './components/NewTaskForm.jsx';

const kBaseUrl = 'http://localhost:5000';
// read the base url from the .env file
const kBaseUrl = import.meta.env.VITE_BASE_URL;
// const kBaseUrl = 'http://localhost:5000';

const taskApiToJson = task => {
// unpack the fields of a task, renaming is_complete to isComplete in the
Expand All @@ -21,21 +24,18 @@ const taskApiToJson = task => {
// then/catch clauses to update its state or do any additional error handling

const getTasksAsync = async () => {
// return the end of the promise chain to allow further then/catch calls
try {
// return the end of the promise chain to allow further then/catch calls
const response = await axios.get(`${kBaseUrl}/tasks`);

// convert the received tasks from having python-like keys to JS-like keys
// using a helper function (taskApiToJson) that will be run on each task
// in the result.

// the value we return from a then will become the input to the next then
return response.data.map(taskApiToJson);

} catch (err) {
console.log(err);

// anything we throw will skip over any intervening then clauses to become
// the input to the next catch clause
// throw a simplified error
throw new Error('error fetching tasks');
}
};
Expand All @@ -49,21 +49,17 @@ const getTasksAsync = async () => {
const updateTaskAsync = async (id, markComplete) => {
const endpoint = markComplete ? 'mark_complete' : 'mark_incomplete';

// return the end of the promise chain to allow further then/catch calls
try {
// return the end of the promise chain to allow further then/catch calls
const response = await axios.patch(`${kBaseUrl}/tasks/${id}/${endpoint}`);

// convert the received task from having python-like keys to JS-like keys
// using a helper function (taskApiToJson)

// the value we return from a then will become the input to the next then
return taskApiToJson(response.data.task);

} catch (err) {
console.log(err);

// anything we throw will skip over any intervening then clauses to become
// the input to the next catch clause
// throw a simplified error
throw new Error(`error updating task ${id}`);
}
};
Expand All @@ -72,21 +68,43 @@ const updateTaskAsync = async (id, markComplete) => {
// call using axios to delete the specified task.

const deleteTaskAsync = async id => {
// return the end of the promise chain to allow further then/catch calls
// note no .then here since there's nothing useful for us to process from the
// response. it returns a status message structure:
// { "details": "Task 3 \"do the other thing\" successfully deleted" }
try {
await axios.delete(`${kBaseUrl}/tasks/${id}`);
} catch (err) {
console.log(err);

// anything we throw will skip over any intervening then clauses to become
// the input to the next catch clause
// throw a simplified error
throw new Error(`error deleting task ${id}`);
}
};

const addTaskAsync = async taskData => {
// extract values from taskData
const { title, isComplete } = taskData;

// compute additional values
const description = 'created in Task List Front End';
const completedAt = isComplete ? new Date() : null;

// build a request body using a string key to avoid having the linter
// yell at us
const body = { title, description, 'completed_at': completedAt };

try {
const response = await axios.post(`${kBaseUrl}/tasks`, body);

// convert the received task from having python-like keys to JS-like keys
// using a helper function (taskApiToJson)
return taskApiToJson(response.data.task);

} catch (err) {
console.log(err);

// throw a simplified error
throw new Error('error creating task');
}
};

const App = () => {
const [tasks, setTasks] = useState([]); // initialize to an empty list of tasks

Expand All @@ -100,7 +118,7 @@ const App = () => {

const refreshTasks = async () => {
try {
const tasks = await getTasksAsync()
const tasks = await getTasksAsync();
setTasks(tasks);
} catch (err) {
console.log(err.message);
Expand All @@ -123,7 +141,6 @@ const App = () => {
// start the async task to toggle the completion
try {
const newTask = await updateTaskAsync(id, !task.isComplete);

// use the callback style of updating the tasks list
// oldTasks will receive the current contents of the tasks state
setTasks(oldTasks => {
Expand Down Expand Up @@ -166,6 +183,19 @@ const App = () => {
}
};

const addTask = async taskData => {
try {
const task = await addTaskAsync(taskData);

// use the callback style of updating the tasks list
// oldTasks will receive the current contents of the tasks state
// this is very short, so we can use the implied return arrow function
setTasks(oldTasks => [ ...oldTasks, task ]);
} catch (err) {
console.log(err.message);
}
};

return (
<div className="App">
<header className="App-header">
Expand All @@ -179,6 +209,9 @@ const App = () => {
onDeleteCallback={deleteTask}
/>
</div>
<div>
<NewTaskForm onAddTaskCallback={addTask} />
</div>
</main>
</div>
);
Expand Down
25 changes: 25 additions & 0 deletions src/components/NewTaskForm.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.new-task__form {
margin-top: 1em;
display: flex;
justify-content: center;
}

.new-task__fields {
display: grid;
grid-template-columns: 1fr 1fr;
width: 400px;
row-gap: 20px;
}

.new-task__fields label {
font-weight: bolder;
font-size: 1.2em;
}

.new-task__submit {
grid-column: span 2;
}

.new-task__form h2 {
font-size: 1.5em;
}
104 changes: 104 additions & 0 deletions src/components/NewTaskForm.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { useState } from 'react';
import PropTypes from 'prop-types';
import './NewTaskForm.css';

// It can be convenient to declare an object of function to represent
// or build the default values for the state when we use an object
// as the state so that it's easy to set/reset
const kNewFormData = {
title: '',
isComplete: 'false',
};

const NewTaskForm = ({ onAddTaskCallback }) => {
const [taskData, setTaskData] = useState(kNewFormData);

const handleChange = (e) => {
const fieldName = e.target.name;
const value = e.target.value;

// the [] around fieldName is not related to arrays. It's
// telling JS to treat the key expression as JS code rather
// than a plain string, so [fieldName]: means to use the value
// stored in fieldName as the key, rather than literally "fieldName"
setTaskData(oldData => ({ ...oldData, [fieldName]: value }));

// Notice the () around the object. Without those, JS would interpret
// the {} as the braces around the function body rather than the
// start of an object literal.
};

const handleSubmit = (e) => {
// prevent the default browser submit action
e.preventDefault();

if (!taskData.title) { return; }

// reset the form back to its default values. This won't affect the value
// of taskData until React re-renders, so we are still free to use it in
// the remainder of this function
setTaskData(kNewFormData);

// use the supplied callback to notify the outside world that we
// have data ready to be used. Notice that we translate the string values
// 'true' and 'false' into a real boolean here. It had been a string
// because we used a select control for picking whether the task was
// complete or not, and the value of a select is a string. We could have
// used a custom handler to response to the select change event rather than
// using the same handler for title and isComplete, but it's relatively
// straightforward to deal with here, as long as we don't forget to do it!
onAddTaskCallback({
...taskData,
isComplete: taskData.isComplete === 'true'
});
};

// it's somewhat unsafe to use id on the title and isComplete input tags.
// while unlikely for this input control, in general, a control might appear
// on page multiple times, while an id should appear only a single time.
// we are using an id in order to the label's htmlFor attribute to properly
// connect to the related label. if we were more concerned about the ids being
// unique across multiple controls, we could use a uuid module to generate
// a "unique" value that we could concatenate into the control ids so that
// we could be sure the id was truly unique on the page.

return (
<form onSubmit={handleSubmit} className="new-task__form">
<section>
<h2>Add a Task</h2>
<div className="new-task__fields">
<label htmlFor="new-task__title">Title</label>
<input
name="title"
id="new-task__title"
value={taskData.title}
onChange={handleChange}
/>
<label htmlFor="new-task__isComplete">Complete</label>
<select
value={taskData.isComplete}
onChange={handleChange}
name="isComplete"
id="new-task__isComplete"
>
<option value="true">
Yes
</option>
<option value="false">
No
</option>
</select>
<button className="button new-task__submit" type="submit">
Add Task
</button>
</div>
</section>
</form>
);
};

NewTaskForm.propTypes = {
onAddTaskCallback: PropTypes.func.isRequired,
};

export default NewTaskForm;