Skip to content

Commit e117c16

Browse files
authored
Merge pull request #25 from Jim-Hodapp-Coaching/create_a_new_note
2 parents 039fc81 + d160686 commit e117c16

File tree

3 files changed

+396
-9
lines changed

3 files changed

+396
-9
lines changed

src/app/coaching-sessions/[id]/page.tsx

Lines changed: 138 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,86 @@ import { cn } from "@/lib/utils";
3939
import { models, types } from "@/data/models";
4040
import { current, future, past } from "@/data/presets";
4141
import { useAppStateStore } from "@/lib/providers/app-state-store-provider";
42-
//import { useAuthStore } from "@/lib/providers/auth-store-provider";
42+
import { useEffect, useState } from "react";
43+
import {
44+
createNote,
45+
fetchNotesByCoachingSessionId,
46+
updateNote,
47+
} from "@/lib/api/notes";
48+
import { Note, noteToString } from "@/types/note";
49+
import { useAuthStore } from "@/lib/providers/auth-store-provider";
50+
import { Id } from "@/types/general";
4351

4452
// export const metadata: Metadata = {
4553
// title: "Coaching Session",
4654
// description: "Coaching session main page, where the good stuff happens.",
4755
// };
4856

4957
export default function CoachingSessionsPage() {
50-
const [isOpen, setIsOpen] = React.useState(false);
51-
//const { isLoggedIn, userId } = useAuthStore((state) => state);
52-
const { organizationId, relationshipId, coachingSessionId } =
53-
useAppStateStore((state) => state);
58+
const [isOpen, setIsOpen] = useState(false);
59+
const [noteId, setNoteId] = useState<Id>("");
60+
const [note, setNote] = useState<string>("");
61+
const [syncStatus, setSyncStatus] = useState<string>("");
62+
const { userId } = useAuthStore((state) => state);
63+
const { coachingSessionId } = useAppStateStore((state) => state);
64+
65+
useEffect(() => {
66+
async function fetchNote() {
67+
if (!coachingSessionId) return;
68+
69+
await fetchNotesByCoachingSessionId(coachingSessionId)
70+
.then((notes) => {
71+
// Apparently it's normal for this to be triggered twice in modern
72+
// React versions in strict + development modes
73+
// https://stackoverflow.com/questions/60618844/react-hooks-useeffect-is-called-twice-even-if-an-empty-array-is-used-as-an-ar
74+
const note = notes[0];
75+
console.trace("note: " + noteToString(note));
76+
setNoteId(note.id);
77+
setNote(note.body);
78+
})
79+
.catch((err) => {
80+
console.error(
81+
"Failed to fetch Note for current coaching session: " + err
82+
);
83+
84+
createNote(coachingSessionId, userId, "")
85+
.then((note) => {
86+
// Apparently it's normal for this to be triggered twice in modern
87+
// React versions in strict + development modes
88+
// https://stackoverflow.com/questions/60618844/react-hooks-useeffect-is-called-twice-even-if-an-empty-array-is-used-as-an-ar
89+
console.trace("New empty note: " + noteToString(note));
90+
setNoteId(note.id);
91+
})
92+
.catch((err) => {
93+
console.error("Failed to create new empty Note: " + err);
94+
});
95+
});
96+
}
97+
fetchNote();
98+
}, [coachingSessionId, !note]);
99+
100+
const handleInputChange = (value: string) => {
101+
setNote(value);
102+
103+
if (noteId && coachingSessionId && userId) {
104+
updateNote(noteId, coachingSessionId, userId, value)
105+
.then((note) => {
106+
// Apparently it's normal for this to be triggered twice in modern
107+
// React versions in strict + development modes
108+
// https://stackoverflow.com/questions/60618844/react-hooks-useeffect-is-called-twice-even-if-an-empty-array-is-used-as-an-ar
109+
console.trace("Updated Note: " + noteToString(note));
110+
setSyncStatus("All changes saved");
111+
})
112+
.catch((err) => {
113+
setSyncStatus("Failed to save changes");
114+
console.error("Failed to update Note: " + err);
115+
});
116+
}
117+
};
118+
119+
const handleKeyDown = () => {
120+
setSyncStatus("");
121+
};
54122

55123
return (
56124
<>
@@ -156,10 +224,14 @@ export default function CoachingSessionsPage() {
156224
</TabsList>
157225
<TabsContent value="notes">
158226
<div className="flex h-full flex-col space-y-4">
159-
<Textarea
160-
placeholder="Session notes"
161-
className="p-4 min-h-[400px] md:min-h-[630px] lg:min-h-[630px]"
162-
/>
227+
<CoachingNotes
228+
value={note}
229+
onChange={handleInputChange}
230+
onKeyDown={handleKeyDown}
231+
></CoachingNotes>
232+
<p className="text-sm text-muted-foreground">
233+
{syncStatus}
234+
</p>
163235
</div>
164236
</TabsContent>
165237
<TabsContent value="program">
@@ -194,3 +266,60 @@ export default function CoachingSessionsPage() {
194266
</>
195267
);
196268
}
269+
270+
// A debounced input CoachingNotes textarea component
271+
// TODO: move this into the components dir
272+
const CoachingNotes: React.FC<{
273+
value: string;
274+
onChange: (value: string) => void;
275+
onKeyDown: () => void;
276+
}> = ({ value, onChange, onKeyDown }) => {
277+
const WAIT_INTERVAL = 1000;
278+
const [timer, setTimer] = useState<number | undefined>(undefined);
279+
const [note, setNote] = useState<string>(value);
280+
281+
// Make sure the internal value prop updates when the component interface's
282+
// value prop changes.
283+
useEffect(() => {
284+
setNote(value);
285+
}, [value]);
286+
287+
const handleSessionNoteChange = (
288+
e: React.ChangeEvent<HTMLTextAreaElement>
289+
) => {
290+
const newValue = e.target.value;
291+
setNote(newValue);
292+
293+
if (timer) {
294+
clearTimeout(timer);
295+
}
296+
297+
const newTimer = window.setTimeout(() => {
298+
onChange(newValue);
299+
}, WAIT_INTERVAL);
300+
301+
setTimer(newTimer);
302+
};
303+
304+
const handleOnKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
305+
onKeyDown();
306+
};
307+
308+
useEffect(() => {
309+
return () => {
310+
if (timer) {
311+
clearTimeout(timer);
312+
}
313+
};
314+
}, [timer]);
315+
316+
return (
317+
<Textarea
318+
placeholder="Session notes"
319+
value={note}
320+
className="p-4 min-h-[400px] md:min-h-[630px] lg:min-h-[630px]"
321+
onChange={handleSessionNoteChange}
322+
onKeyDown={handleOnKeyDown}
323+
/>
324+
);
325+
};

src/lib/api/notes.ts

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
// Interacts with the note endpoints
2+
3+
import { Id } from "@/types/general";
4+
import { defaultNote, isNote, isNoteArray, Note, noteToString, parseNote } from "@/types/note";
5+
import { AxiosError, AxiosResponse } from "axios";
6+
7+
export const fetchNotesByCoachingSessionId = async (
8+
coachingSessionId: Id
9+
): Promise<Note[]> => {
10+
const axios = require("axios");
11+
12+
var notes: Note[] = [];
13+
var err: string = "";
14+
15+
const data = await axios
16+
.get(`http://localhost:4000/notes`, {
17+
params: {
18+
coaching_session_id: coachingSessionId,
19+
},
20+
withCredentials: true,
21+
setTimeout: 5000, // 5 seconds before timing out trying to log in with the backend
22+
headers: {
23+
"X-Version": "0.0.1",
24+
},
25+
})
26+
.then(function (response: AxiosResponse) {
27+
// handle success
28+
if (response?.status == 204) {
29+
console.error("Retrieval of Note failed: no content.");
30+
err = "Retrieval of Note failed: no content.";
31+
} else {
32+
var notes_data = response.data.data;
33+
if (isNoteArray(notes_data)) {
34+
notes_data.forEach((note_data: any) => {
35+
notes.push(parseNote(note_data))
36+
});
37+
}
38+
}
39+
})
40+
.catch(function (error: AxiosError) {
41+
// handle error
42+
console.error(error.response?.status);
43+
if (error.response?.status == 401) {
44+
console.error("Retrieval of Note failed: unauthorized.");
45+
err = "Retrieval of Note failed: unauthorized.";
46+
} else {
47+
console.log(error);
48+
console.error(
49+
`Retrieval of Note by coaching session Id (` + coachingSessionId + `) failed.`
50+
);
51+
err =
52+
`Retrieval of Note by coaching session Id (` + coachingSessionId + `) failed.`;
53+
}
54+
});
55+
56+
if (err)
57+
throw err;
58+
59+
return notes;
60+
};
61+
62+
export const createNote = async (
63+
coaching_session_id: Id,
64+
user_id: Id,
65+
body: string
66+
): Promise<Note> => {
67+
const axios = require("axios");
68+
69+
const newNoteJson = {
70+
coaching_session_id: coaching_session_id,
71+
user_id: user_id,
72+
body: body
73+
};
74+
console.debug("newNoteJson: " + JSON.stringify(newNoteJson));
75+
// A full real note to be returned from the backend with the same body
76+
var createdNote: Note = defaultNote();
77+
var err: string = "";
78+
79+
//var strNote: string = noteToString(note);
80+
const data = await axios
81+
.post(`http://localhost:4000/notes`, newNoteJson, {
82+
withCredentials: true,
83+
setTimeout: 5000, // 5 seconds before timing out trying to log in with the backend
84+
headers: {
85+
"X-Version": "0.0.1",
86+
"Content-Type": "application/json",
87+
},
88+
})
89+
.then(function (response: AxiosResponse) {
90+
// handle success
91+
const noteStr = response.data.data;
92+
if (isNote(noteStr)) {
93+
createdNote = parseNote(noteStr);
94+
}
95+
})
96+
.catch(function (error: AxiosError) {
97+
// handle error
98+
console.error(error.response?.status);
99+
if (error.response?.status == 401) {
100+
console.error("Creation of Note failed: unauthorized.");
101+
err = "Creation of Note failed: unauthorized.";
102+
} else if (error.response?.status == 500) {
103+
console.error(
104+
"Creation of Note failed: internal server error."
105+
);
106+
err = "Creation of Note failed: internal server error.";
107+
} else {
108+
console.log(error);
109+
console.error(`Creation of new Note failed.`);
110+
err = `Creation of new Note failed.`;
111+
}
112+
}
113+
);
114+
115+
if (err)
116+
throw err;
117+
118+
return createdNote;
119+
};
120+
121+
export const updateNote = async (
122+
id: Id,
123+
user_id: Id,
124+
coaching_session_id: Id,
125+
body: string,
126+
): Promise<Note> => {
127+
const axios = require("axios");
128+
129+
var updatedNote: Note = defaultNote();
130+
var err: string = "";
131+
132+
const newNoteJson = {
133+
coaching_session_id: coaching_session_id,
134+
user_id: user_id,
135+
body: body
136+
};
137+
138+
const data = await axios
139+
.put(`http://localhost:4000/notes/${id}`, newNoteJson, {
140+
withCredentials: true,
141+
setTimeout: 5000, // 5 seconds before timing out trying to log in with the backend
142+
headers: {
143+
"X-Version": "0.0.1",
144+
"Content-Type": "application/json",
145+
},
146+
})
147+
.then(function (response: AxiosResponse) {
148+
// handle success
149+
if (isNote(response.data.data)) {
150+
updatedNote = response.data.data;
151+
}
152+
})
153+
.catch(function (error: AxiosError) {
154+
// handle error
155+
console.error(error.response?.status);
156+
if (error.response?.status == 401) {
157+
console.error("Update of Note failed: unauthorized.");
158+
err = "Update of Organization failed: unauthorized.";
159+
} else if (error.response?.status == 500) {
160+
console.error("Update of Organization failed: internal server error.");
161+
err = "Update of Organization failed: internal server error.";
162+
} else {
163+
console.log(error);
164+
console.error(`Update of new Organization failed.`);
165+
err = `Update of new Organization failed.`;
166+
}
167+
});
168+
169+
if (err)
170+
throw err;
171+
172+
return updatedNote;
173+
};

0 commit comments

Comments
 (0)