Skip to content

Commit e069002

Browse files
committed
Implement subtask viewing, creation, deletion
* Implement RemoveableListItem - A list item that has a remove button * Use RemoveableListItem in ShareList and SubtaskList components * Implement deleteTask for convenience when deleting subtask * Implement createSubtask in requests and call * Update UI automatically for creation and deletion * Implement interface for viewing subtasks side-by-side with task details * Fix useEscapeAlerter bug - wrap in useEffect() to make it a real hook
1 parent eedaa8f commit e069002

13 files changed

Lines changed: 241 additions & 83 deletions

File tree

components/CreateJottingButton/createJottingButton.js

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import createJottingBtn from "./createJottingBtn.module.css";
22
import { useState, useRef } from "react";
3-
import { createJotting as createJot } from "../../libs/Datastore/requests";
3+
import * as Requests from "../../libs/Datastore/requests";
44

5-
export default function CreateJottingButton({ jotType, jots, setJots }) {
5+
export default function CreateJottingButton({ jotType, jots, setJots, requestFunc="createJotting", requestArg1=jotType }) {
66
const [newJotInputCls, setNewJotInputCls] = useState("invisible");
77
const [createJotBtnCls, setCreateJotBtnCls] = useState(
88
createJottingBtn.createJottingBtn
@@ -44,10 +44,16 @@ export default function CreateJottingButton({ jotType, jots, setJots }) {
4444

4545
if (e.key == "Enter") {
4646
const jotName = e.target.value;
47-
const response = await createJot(jotType, jotName);
48-
clearInput();
49-
setJots([...jots, response]);
50-
showBtn();
47+
try {
48+
const response = await Requests[requestFunc](requestArg1, jotName);
49+
setJots([...jots, {...response, title: jotName}]);
50+
} catch (e) {
51+
console.error(e);
52+
window.alert(e);
53+
} finally {
54+
clearInput();
55+
showBtn();
56+
}
5157
} else if (e.key == "Escape") {
5258
clearInput();
5359
toggleJotEl();

components/Jotting/jotting.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import JottingTitle from "./jottingTitle";
33

44
export default function Jotting(props) {
55
return (
6-
<div className={jotting.fullJotting}>
6+
<div className={`${jotting.fullJotting} ${jotting[props.jotType]}`}>
77
<JottingTitle
88
id={props.id}
99
originalTitle={props.title}

components/Jotting/jotting.module.css

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,39 +11,33 @@
1111

1212
.fullJotting {
1313
padding: 10px 0;
14-
display: flex;
15-
flex-direction: column;
14+
display: grid;
15+
grid-template-rows: auto 100px 1fr;
16+
grid-template-columns: repeat(4, 1fr);
17+
background-color: rgb(124, 252, 252);
18+
padding: 2% 5%;
19+
width: 100%;
1620
height: 100%;
17-
justify-content: flex-start;
18-
background-color: rgba(124, 252, 252, 0.993);
19-
}
20-
21-
.details {
22-
font-family: sans-serif;
23-
background: none;
24-
border: none;
25-
resize: none;
26-
padding: 1em;
27-
height: 300px;
21+
column-gap: 1em;
2822
}
2923

3024
.title {
25+
grid-row: 1;
26+
grid-column: 1 / -1;
3127
border: none;
3228
background: none;
3329
font-size: 2em;
3430
text-align: center;
3531
}
3632

37-
.title, .details {
38-
margin: 0 5%;
39-
}
4033

4134
.jottingOptionsBar {
35+
grid-row: 2;
36+
grid-column: 1 / -1;
4237
display: flex;
4338
flex-direction: row;
4439
justify-content: center;
4540
align-items: center;
46-
height: 100px;
4741
}
4842

4943
.jottingOptionsBar button {
@@ -55,4 +49,41 @@
5549

5650
.jottingOptionsBar button:hover {
5751
scale: 1.2;
52+
}
53+
54+
.details {
55+
grid-row: 3;
56+
grid-column: 1;
57+
font-family: sans-serif;
58+
background: none;
59+
border: none;
60+
resize: none;
61+
padding: 1em;
62+
height: 300px;
63+
overflow: auto;
64+
scrollbar-color: dark indianred;
65+
}
66+
67+
.note .details {
68+
grid-column-end: 4;
69+
}
70+
71+
.task .details {
72+
grid-column-end: 3;
73+
}
74+
75+
.subtasksControl {
76+
grid-row: 3;
77+
grid-column: 3 / 5;
78+
display: flex;
79+
flex-direction: column;
80+
}
81+
82+
.subtasks {
83+
padding-left: 0;
84+
height: 100%;
85+
}
86+
87+
.subtasksControl:last-child {
88+
margin-bottom: 11%;
5889
}

components/Jotting/subtaskList.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { useEffect, useState } from "react";
2+
import { deleteTask, getSubtasks } from "../../libs/Datastore/requests";
3+
import CircularProgress from "../CircularProgress/circularProgress";
4+
import FetchError from "../FetchError/fetchError";
5+
import RemoveableListItem from "../RemoveableListItem/removeableListItem";
6+
import jotting from "./jotting.module.css";
7+
8+
/**
9+
* @param { {id : number } } props The info about the task
10+
* @param {number} props.id The id of the parent task
11+
*/
12+
export default function SubtaskList({ id, subtasksState }) {
13+
const [subtasks, setSubtasks] = subtasksState;
14+
15+
// componentDidMount() - initally request subtasks
16+
useEffect(() => {
17+
(async function () {
18+
try {
19+
const initialSubtasks = await getSubtasks(id);
20+
setSubtasks(initialSubtasks);
21+
} catch {}
22+
})();
23+
}, []);
24+
25+
const handleRemoveClick = (e) => {
26+
const itemId = e.target.parentElement.id.substring(2);
27+
deleteTask(itemId)
28+
.then(() => {
29+
for (let i = 0; i < subtasks.length; i++) {
30+
if (subtasks[i].id == itemId) {
31+
const newList = subtasks; // Temporarily store list to prevent state mutation
32+
newList.splice(i, 1); // Remove the element at the current index
33+
setSubtasks(null); // Needed for state to actually update
34+
setSubtasks(newList);
35+
return;
36+
}
37+
}
38+
})
39+
.catch(alert);
40+
};
41+
42+
if (subtasks instanceof Array)
43+
return (
44+
<ul className={jotting.subtasks}>
45+
{subtasks.map((item) => (
46+
<RemoveableListItem
47+
key={item.id}
48+
handleRemoveClick={handleRemoveClick}
49+
removeText="X"
50+
id={"s-" + item.id}
51+
content={item.title}
52+
/>
53+
))}
54+
</ul>
55+
);
56+
else if (subtasks != null) return <FetchError itemName="subtasks" />;
57+
else return <CircularProgress />;
58+
}

components/Jotting/task.js

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,34 @@
1+
import jotting from "./jotting.module.css";
12
import Jotting from "./jotting";
23
import JottingDetails from "./jottingDetails";
4+
import JottingOptionsBar from "./JottingOptionsBar";
5+
import SubtaskList from "./subtaskList";
6+
import CreateJottingButton from "../CreateJottingButton/createJottingButton";
7+
import { useState } from "react";
38

49
export default function Task(task) {
5-
console.log(task);
6-
return (
7-
<Jotting jotType="task" {...task}>
8-
<JottingDetails jotType="task" jottingInfo={task} />
9-
</Jotting>
10-
);
11-
}
10+
return (
11+
<Jotting jotType="task" {...task}>
12+
<JottingOptionsBar {...task} />
13+
<JottingDetails jotType="task" jottingInfo={task} />
14+
<SubtasksControl task={task} />
15+
</Jotting>
16+
);
17+
}
18+
19+
function SubtasksControl({task}) {
20+
const [subtasks, setSubtasks] = useState(null);
21+
return (
22+
<div className={jotting.subtasksControl}>
23+
<h3>Subtasks</h3>
24+
<SubtaskList id={task.id} subtasksState={[subtasks, setSubtasks]}/>
25+
<CreateJottingButton
26+
requestFunc="createSubtask"
27+
jotType="subtask"
28+
jots={subtasks}
29+
setJots={setSubtasks}
30+
requestArg1={task.id}
31+
/>
32+
</div>
33+
);
34+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import removeableListItem from "./removeableListitem.module.css";
2+
3+
/**
4+
* A list item that has a remove button
5+
* @param {object} props
6+
* @param {string} props.itemType The type of item in TITLE case
7+
* @param {(e) => void} props.handleRemoveClick The event handler that fires when the remove button is clicked
8+
*/
9+
export default function RemoveableListItem({
10+
handleRemoveClick,
11+
content,
12+
removeText = "Remove",
13+
id
14+
}) {
15+
return (
16+
<li id={id} className={removeableListItem.item}>
17+
<span>{content}</span>
18+
<button
19+
onClick={handleRemoveClick}
20+
className={removeableListItem.removeItemButton}
21+
>
22+
{removeText}
23+
</button>
24+
</li>
25+
);
26+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
.item {
2+
width: 100%;
3+
}
4+
5+
.item > span {
6+
display: inline-block;
7+
width: 80%;
8+
}
9+
10+
.removeItemButton {
11+
margin: 1em;
12+
background: none;
13+
border: none;
14+
color: red;
15+
text-align: right;
16+
width: calc(20% - 2em);
17+
}

components/ShareMenu/shareMenu.js

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,30 +11,38 @@ export default function ShareMenu() {
1111

1212
const ref = useRef(null);
1313
const router = useRouter();
14-
14+
1515
const abortController = new AbortController();
16-
16+
1717
const handleShareClick = (e) => {
18-
shareNote(router.query.id, recipientEmail, abortController)
1918
setRecipientEmail("");
20-
setNoteSharees([...noteSharees, {
21-
email: recipientEmail,
22-
user_id: 0
23-
}]);
19+
shareNote(router.query.id, recipientEmail, abortController).then(
20+
(response) =>
21+
setNoteSharees([
22+
...noteSharees,
23+
{
24+
email: recipientEmail,
25+
user_id: response.data.recipient_id,
26+
},
27+
])
28+
);
2429
};
25-
30+
2631
const handleRecipientEmailChange = (e) => setRecipientEmail(e.target.value);
27-
32+
2833
useEffect(() => {
2934
ref.current.focus();
3035
}, []);
31-
36+
3237
useCancelableRequest(getNoteSharees, setNoteSharees, [router.query.id], []);
33-
38+
3439
return (
3540
<div className={shareMenu.shareMenu}>
3641
<h1>Share</h1>
37-
<ShareeList noteSharees={noteSharees} setNoteSharees={setNoteSharees} />
42+
<ShareeList
43+
noteSharees={noteSharees}
44+
setNoteSharees={setNoteSharees}
45+
/>
3846
<input
3947
type="text"
4048
ref={ref}
@@ -46,4 +54,4 @@ export default function ShareMenu() {
4654
<button onClick={handleShareClick}>Share</button>
4755
</div>
4856
);
49-
}
57+
}

components/ShareeList/shareeList.js

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { removeSharee } from "../../libs/Datastore/requests";
33
import ProgressSpinner from "../CircularProgress/circularProgress";
44
import FetchError from "../FetchError/fetchError";
55
import shareeList from "./shareeList.module.css";
6+
import RemoveableListItem from "../RemoveableListItem/removeableListItem";
67

78
/**
89
*
@@ -27,7 +28,7 @@ export default function ShareeList({ noteSharees, setNoteSharees }) {
2728
{noteSharees.map((item) => (
2829
<ShareeListItem
2930
removeShareeById={removeShareeById}
30-
key={"U" + item.user_id}
31+
key={item.user_id}
3132
{...item}
3233
/>
3334
))}
@@ -57,15 +58,5 @@ function ShareeListItem({ user_id, email, removeShareeById }) {
5758
);
5859
};
5960

60-
return (
61-
<li className={shareeList.item}>
62-
<span>{email}</span>
63-
<button
64-
onClick={handleRemoveClick}
65-
className={shareeList.removeItemButton}
66-
>
67-
Remove
68-
</button>
69-
</li>
70-
);
61+
return <RemoveableListItem handleRemoveClick={handleRemoveClick} content={email} />
7162
}

components/ShareeList/shareeList.module.css

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,4 @@
66
.shareeList, .nonList {
77
padding: 0 20px;
88
height: 100%;
9-
}
10-
11-
.item {
12-
width: 100%;
13-
}
14-
15-
.item > span {
16-
display: inline-block;
17-
width: 80%;
18-
}
19-
20-
.removeItemButton {
21-
margin: 1em;
22-
background: none;
23-
border: none;
24-
color: red;
25-
text-align: right;
269
}

0 commit comments

Comments
 (0)