Skip to content

Commit 5c3a9a9

Browse files
committed
Implement subtask completion viewing
* Implement StyledCheckbox component - styleable checkbox - Instantaneously updates UI but undoes UI change if there is an error * Make RemoveableListItem a composed component (uses props.children) * Implement updateTaskStatus * Add PinImage to NotePreview
1 parent 35953b8 commit 5c3a9a9

9 files changed

Lines changed: 196 additions & 83 deletions

File tree

components/CreateJottingButton/createJottingButton.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ import createJottingBtn from "./createJottingBtn.module.css";
22
import { useState, useRef } from "react";
33
import * as Requests from "../../libs/Datastore/requests";
44

5-
export default function CreateJottingButton({ jotType, jots, setJots, requestFunc="createJotting", requestArg1=jotType }) {
5+
export default function CreateJottingButton({
6+
jotType,
7+
jots,
8+
setJots,
9+
requestFunc = "createJotting",
10+
requestArg1 = jotType,
11+
}) {
612
const [newJotInputCls, setNewJotInputCls] = useState("invisible");
713
const [createJotBtnCls, setCreateJotBtnCls] = useState(
814
createJottingBtn.createJottingBtn
@@ -45,8 +51,11 @@ export default function CreateJottingButton({ jotType, jots, setJots, requestFun
4551
if (e.key == "Enter") {
4652
const jotName = e.target.value;
4753
try {
48-
const response = await Requests[requestFunc](requestArg1, jotName);
49-
setJots([...jots, {...response, title: jotName}]);
54+
const response = await Requests[requestFunc](
55+
requestArg1,
56+
jotName
57+
);
58+
setJots([...jots, { ...response, title: jotName }]);
5059
} catch (e) {
5160
console.error(e);
5261
window.alert(e);
Lines changed: 57 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,100 @@
11
.jotting button {
2-
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
3-
font-size: 1rem;
4-
background: none;
5-
border: none;
6-
text-align: left;
7-
padding: 1em;
8-
width: 100%;
9-
height: 100%;
2+
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
3+
font-size: 1rem;
4+
background: none;
5+
border: none;
6+
text-align: left;
7+
padding: 1em;
8+
width: 100%;
9+
height: 100%;
1010
}
1111

1212
.fullJotting {
13-
display: grid;
14-
grid-template-rows: 50px 100px 300px;
15-
grid-template-columns: repeat(4, 1fr);
16-
background-color: rgb(124, 252, 252);
17-
padding: 2% 5%;
18-
width: 100%;
19-
height: 500px;
20-
column-gap: 1em;
13+
display: grid;
14+
grid-template-rows: 50px 100px 300px;
15+
grid-template-columns: repeat(4, 1fr);
16+
background-color: rgb(124, 252, 252);
17+
padding: 2% 5%;
18+
width: 100%;
19+
height: 500px;
20+
column-gap: 1em;
2121
}
2222

2323
.title {
24-
grid-row: 1;
25-
grid-column: 1 / -1;
26-
border: none;
27-
background: none;
28-
font-size: 2em;
29-
text-align: center;
24+
grid-row: 1;
25+
grid-column: 1 / -1;
26+
border: none;
27+
background: none;
28+
font-size: 2em;
29+
text-align: center;
3030
}
3131

3232
.previewButton {
33-
display: flex;
34-
align-items: center;
33+
display: flex;
34+
align-items: center;
3535
}
3636

3737
.previewTitle {
38-
display: inline-block;
39-
width: 170px;
38+
display: inline-block;
39+
width: 170px;
4040
}
4141

4242
.previewPin {
4343
}
4444

4545
.jottingOptionsBar {
46-
grid-row: 2;
47-
grid-column: 1 / -1;
48-
display: flex;
49-
flex-direction: row;
50-
justify-content: center;
51-
align-items: center;
46+
grid-row: 2;
47+
grid-column: 1 / -1;
48+
display: flex;
49+
flex-direction: row;
50+
justify-content: center;
51+
align-items: center;
5252
}
5353

5454
.jottingOptionsBar button {
55-
background: none;
56-
border: none;
57-
margin: 0 1em;
58-
padding: 1em;
55+
background: none;
56+
border: none;
57+
margin: 0 1em;
58+
padding: 1em;
5959
}
6060

6161
.jottingOptionsBar button:hover {
62-
scale: 1.2;
62+
scale: 1.2;
6363
}
6464

6565
.details {
66-
grid-row: 3;
67-
grid-column: 1;
68-
font-family: sans-serif;
69-
background: none;
70-
border: none;
71-
resize: none;
72-
padding: 1em;
73-
height: 300px;
74-
overflow: auto;
75-
scrollbar-color: dark indianred;
66+
grid-row: 3;
67+
grid-column: 1;
68+
font-family: sans-serif;
69+
background: none;
70+
border: none;
71+
resize: none;
72+
padding: 1em;
73+
height: 300px;
74+
overflow: auto;
75+
scrollbar-color: dark indianred;
7676
}
7777

7878
.note .details {
79-
grid-column-end: 4;
79+
grid-column-end: 4;
8080
}
8181

8282
.task .details {
83-
grid-column-end: 3;
83+
grid-column-end: 3;
8484
}
8585

8686
.subtasksControl {
87-
grid-row: 3;
88-
grid-column: 3 / 5;
89-
display: flex;
90-
flex-direction: column;
87+
grid-row: 3;
88+
grid-column: 3 / 5;
89+
display: flex;
90+
flex-direction: column;
9191
}
9292

9393
.subtaskHeading {
94-
margin: 0 0 3px 0;
94+
margin: 0 0 3px 0;
9595
}
9696

9797
.subtasks {
98-
padding-left: 0;
99-
overflow: auto;
100-
}
98+
padding-left: 0;
99+
overflow: auto;
100+
}

components/Jotting/notePreview.js

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import jotting from "./jotting.module.css";
22
import Jotting from "../../libs/Jotting";
33
import { useRouter } from "next/router";
4-
import Image from "next/image";
4+
import PinImage from "../pinImage";
55

66
export default function NotePreview(props) {
77
const router = useRouter();
@@ -14,13 +14,7 @@ export default function NotePreview(props) {
1414
<data className={jotting.jotting} value={"N" + props.id}>
1515
<button onClick={handleNoteBtnClick}>
1616
<span className={jotting.previewTitle}>{props.title}</span>
17-
{props.priority == 1 ? <Image
18-
height={32}
19-
width={32}
20-
src="/images/pin.png"
21-
alt="Pin icon"
22-
title="This jotting is pinned"
23-
/> : ""}
17+
{props.priority == 1 ? <PinImage className={jotting.previewPin} /> : ""}
2418
</button>
2519
</data>
2620
);

components/Jotting/subtaskList.js

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { deleteTask, getSubtasks } from "../../libs/Datastore/requests";
33
import CircularProgress from "../CircularProgress/circularProgress";
44
import FetchError from "../FetchError/fetchError";
55
import RemoveableListItem from "../RemoveableListItem/removeableListItem";
6+
import StyledCheckbox from "../StyledCheckbox/styledCheckbox";
67
import jotting from "./jotting.module.css";
78

89
/**
@@ -28,7 +29,7 @@ export default function SubtaskList({ id, subtasksState }) {
2829
.then(() => {
2930
for (let i = 0; i < subtasks.length; i++) {
3031
if (subtasks[i].id == itemId) {
31-
const newList = subtasks; // Temporarily store list to prevent state mutation
32+
const newList = subtasks; // Temporarily store list to prevent state mutation
3233
newList.splice(i, 1); // Remove the element at the current index
3334
setSubtasks(null); // Needed for state to actually update
3435
setSubtasks(newList);
@@ -42,15 +43,23 @@ export default function SubtaskList({ id, subtasksState }) {
4243
if (subtasks instanceof Array)
4344
return (
4445
<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-
))}
46+
{subtasks.map((item) => {
47+
return (
48+
<RemoveableListItem
49+
key={item.id}
50+
handleRemoveClick={handleRemoveClick}
51+
removeText="X"
52+
id={"s-" + item.id}
53+
content={item.title}
54+
>
55+
<StyledCheckbox
56+
id={item.id}
57+
prefix="subtask-completion-box-"
58+
completed={item.completed == 1}
59+
/>
60+
</RemoveableListItem>
61+
);
62+
})}
5463
</ul>
5564
);
5665
else if (subtasks != null) return <FetchError itemName="subtasks" />;

components/RemoveableListItem/removeableListItem.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ export default function RemoveableListItem({
1010
handleRemoveClick,
1111
content,
1212
removeText = "Remove",
13-
id
13+
id,
14+
children
1415
}) {
1516
return (
1617
<li id={id} className={removeableListItem.item}>
18+
{children}
1719
<span>{content}</span>
1820
<button
1921
onClick={handleRemoveClick}

components/RemoveableListItem/removeableListItem.module.css

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,4 @@
1313
border: none;
1414
color: red;
1515
text-align: right;
16-
width: calc(20% - 2em);
1716
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import styledCheckbox from "./styledCheckbox.module.css";
2+
import { useEffect, useRef, useState } from "react";
3+
import { updateTaskStatus } from "../../libs/Datastore/requests";
4+
5+
/**
6+
* @param {object} props
7+
* @param {number} props.id
8+
* @param {string} props.prefix
9+
* @param {boolean} props.completed
10+
*/
11+
export default function StyledCheckbox({ id, prefix, completed }) {
12+
const [checked, setChecked] = useState(completed);
13+
const ref = useRef(null);
14+
15+
/**
16+
* Event handler for when the checkbox is checked or unchecked
17+
* @description Uses whether the label's background position contains a negative sign
18+
* to determine whether the checkbox is currently checked
19+
* Instantaneously updates UI but undoes checkbox and alerts if there is a fetch error,
20+
* using Promise.race()
21+
* @param {Event} e
22+
*/
23+
const handleCheckboxChange = (e) => {
24+
const newCheckboxValue = !checked;
25+
Promise.race([
26+
updateTaskStatus(id, newCheckboxValue).catch((err) => {
27+
setChecked(!newCheckboxValue);
28+
alert(err);
29+
}),
30+
setChecked(newCheckboxValue),
31+
]);
32+
};
33+
34+
useEffect(() => {
35+
ref.current.style.backgroundPosition = checked
36+
? "0px -20px"
37+
: "0px 0px";
38+
}, [checked]);
39+
40+
return (
41+
<React.Fragment>
42+
<input
43+
id={`${prefix}${id}`}
44+
className={styledCheckbox.taskCompletionCheckbox}
45+
type="checkbox"
46+
checked={checked}
47+
onChange={handleCheckboxChange}
48+
/>
49+
<label
50+
ref={ref}
51+
htmlFor={`${prefix}${id}`}
52+
className={styledCheckbox.taskCompletionCheckbox}
53+
title="Mark as complete"
54+
></label>
55+
</React.Fragment>
56+
);
57+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/* Source: http://www.csscheckbox.com/ Style the checkbox using an absolutely positioned label */
2+
3+
input.taskCompletionCheckbox:checked + .taskCompletionCheckbox {
4+
background-position: 0 -20px;
5+
}
6+
7+
input.taskCompletionCheckbox + .taskCompletionCheckbox {
8+
padding-left: 22px;
9+
height: 20px;
10+
display: inline-block;
11+
line-height: 20px;
12+
background-repeat: no-repeat;
13+
background-position: 0 0;
14+
font-size: 15px;
15+
vertical-align: middle;
16+
cursor: pointer;
17+
margin-right: 10px;
18+
background-image: url(http://csscheckbox.com/checkboxes/vlad.png);
19+
}
20+
21+
input.taskCompletionCheckbox {
22+
position: absolute;
23+
overflow: hidden;
24+
clip: rect(0 0 0 0);
25+
height: 1px;
26+
width: 1px;
27+
margin: -1px;
28+
padding: 0;
29+
border: 0;
30+
}

0 commit comments

Comments
 (0)