Skip to content

Commit

Permalink
Merge pull request #25 from Jim-Hodapp-Coaching/create_a_new_note
Browse files Browse the repository at this point in the history
  • Loading branch information
jhodapp committed Aug 1, 2024
2 parents 039fc81 + d160686 commit e117c16
Show file tree
Hide file tree
Showing 3 changed files with 396 additions and 9 deletions.
147 changes: 138 additions & 9 deletions src/app/coaching-sessions/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,86 @@ import { cn } from "@/lib/utils";
import { models, types } from "@/data/models";
import { current, future, past } from "@/data/presets";
import { useAppStateStore } from "@/lib/providers/app-state-store-provider";
//import { useAuthStore } from "@/lib/providers/auth-store-provider";
import { useEffect, useState } from "react";
import {
createNote,
fetchNotesByCoachingSessionId,
updateNote,
} from "@/lib/api/notes";
import { Note, noteToString } from "@/types/note";
import { useAuthStore } from "@/lib/providers/auth-store-provider";
import { Id } from "@/types/general";

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

export default function CoachingSessionsPage() {
const [isOpen, setIsOpen] = React.useState(false);
//const { isLoggedIn, userId } = useAuthStore((state) => state);
const { organizationId, relationshipId, coachingSessionId } =
useAppStateStore((state) => state);
const [isOpen, setIsOpen] = useState(false);
const [noteId, setNoteId] = useState<Id>("");
const [note, setNote] = useState<string>("");
const [syncStatus, setSyncStatus] = useState<string>("");
const { userId } = useAuthStore((state) => state);
const { coachingSessionId } = useAppStateStore((state) => state);

useEffect(() => {
async function fetchNote() {
if (!coachingSessionId) return;

await fetchNotesByCoachingSessionId(coachingSessionId)
.then((notes) => {
// Apparently it's normal for this to be triggered twice in modern
// React versions in strict + development modes
// https://stackoverflow.com/questions/60618844/react-hooks-useeffect-is-called-twice-even-if-an-empty-array-is-used-as-an-ar
const note = notes[0];
console.trace("note: " + noteToString(note));
setNoteId(note.id);
setNote(note.body);
})
.catch((err) => {
console.error(
"Failed to fetch Note for current coaching session: " + err
);

createNote(coachingSessionId, userId, "")
.then((note) => {
// Apparently it's normal for this to be triggered twice in modern
// React versions in strict + development modes
// https://stackoverflow.com/questions/60618844/react-hooks-useeffect-is-called-twice-even-if-an-empty-array-is-used-as-an-ar
console.trace("New empty note: " + noteToString(note));
setNoteId(note.id);
})
.catch((err) => {
console.error("Failed to create new empty Note: " + err);
});
});
}
fetchNote();
}, [coachingSessionId, !note]);

const handleInputChange = (value: string) => {
setNote(value);

if (noteId && coachingSessionId && userId) {
updateNote(noteId, coachingSessionId, userId, value)
.then((note) => {
// Apparently it's normal for this to be triggered twice in modern
// React versions in strict + development modes
// https://stackoverflow.com/questions/60618844/react-hooks-useeffect-is-called-twice-even-if-an-empty-array-is-used-as-an-ar
console.trace("Updated Note: " + noteToString(note));
setSyncStatus("All changes saved");
})
.catch((err) => {
setSyncStatus("Failed to save changes");
console.error("Failed to update Note: " + err);
});
}
};

const handleKeyDown = () => {
setSyncStatus("");
};

return (
<>
Expand Down Expand Up @@ -156,10 +224,14 @@ export default function CoachingSessionsPage() {
</TabsList>
<TabsContent value="notes">
<div className="flex h-full flex-col space-y-4">
<Textarea
placeholder="Session notes"
className="p-4 min-h-[400px] md:min-h-[630px] lg:min-h-[630px]"
/>
<CoachingNotes
value={note}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
></CoachingNotes>
<p className="text-sm text-muted-foreground">
{syncStatus}
</p>
</div>
</TabsContent>
<TabsContent value="program">
Expand Down Expand Up @@ -194,3 +266,60 @@ export default function CoachingSessionsPage() {
</>
);
}

// A debounced input CoachingNotes textarea component
// TODO: move this into the components dir
const CoachingNotes: React.FC<{
value: string;
onChange: (value: string) => void;
onKeyDown: () => void;
}> = ({ value, onChange, onKeyDown }) => {
const WAIT_INTERVAL = 1000;
const [timer, setTimer] = useState<number | undefined>(undefined);
const [note, setNote] = useState<string>(value);

// Make sure the internal value prop updates when the component interface's
// value prop changes.
useEffect(() => {
setNote(value);
}, [value]);

const handleSessionNoteChange = (
e: React.ChangeEvent<HTMLTextAreaElement>
) => {
const newValue = e.target.value;
setNote(newValue);

if (timer) {
clearTimeout(timer);
}

const newTimer = window.setTimeout(() => {
onChange(newValue);
}, WAIT_INTERVAL);

setTimer(newTimer);
};

const handleOnKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
onKeyDown();
};

useEffect(() => {
return () => {
if (timer) {
clearTimeout(timer);
}
};
}, [timer]);

return (
<Textarea
placeholder="Session notes"
value={note}
className="p-4 min-h-[400px] md:min-h-[630px] lg:min-h-[630px]"
onChange={handleSessionNoteChange}
onKeyDown={handleOnKeyDown}
/>
);
};
173 changes: 173 additions & 0 deletions src/lib/api/notes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// Interacts with the note endpoints

import { Id } from "@/types/general";
import { defaultNote, isNote, isNoteArray, Note, noteToString, parseNote } from "@/types/note";
import { AxiosError, AxiosResponse } from "axios";

export const fetchNotesByCoachingSessionId = async (
coachingSessionId: Id
): Promise<Note[]> => {
const axios = require("axios");

var notes: Note[] = [];
var err: string = "";

const data = await axios
.get(`http://localhost:4000/notes`, {
params: {
coaching_session_id: coachingSessionId,
},
withCredentials: true,
setTimeout: 5000, // 5 seconds before timing out trying to log in with the backend
headers: {
"X-Version": "0.0.1",
},
})
.then(function (response: AxiosResponse) {
// handle success
if (response?.status == 204) {
console.error("Retrieval of Note failed: no content.");
err = "Retrieval of Note failed: no content.";
} else {
var notes_data = response.data.data;
if (isNoteArray(notes_data)) {
notes_data.forEach((note_data: any) => {
notes.push(parseNote(note_data))
});
}
}
})
.catch(function (error: AxiosError) {
// handle error
console.error(error.response?.status);
if (error.response?.status == 401) {
console.error("Retrieval of Note failed: unauthorized.");
err = "Retrieval of Note failed: unauthorized.";
} else {
console.log(error);
console.error(
`Retrieval of Note by coaching session Id (` + coachingSessionId + `) failed.`
);
err =
`Retrieval of Note by coaching session Id (` + coachingSessionId + `) failed.`;
}
});

if (err)
throw err;

return notes;
};

export const createNote = async (
coaching_session_id: Id,
user_id: Id,
body: string
): Promise<Note> => {
const axios = require("axios");

const newNoteJson = {
coaching_session_id: coaching_session_id,
user_id: user_id,
body: body
};
console.debug("newNoteJson: " + JSON.stringify(newNoteJson));
// A full real note to be returned from the backend with the same body
var createdNote: Note = defaultNote();
var err: string = "";

//var strNote: string = noteToString(note);
const data = await axios
.post(`http://localhost:4000/notes`, newNoteJson, {
withCredentials: true,
setTimeout: 5000, // 5 seconds before timing out trying to log in with the backend
headers: {
"X-Version": "0.0.1",
"Content-Type": "application/json",
},
})
.then(function (response: AxiosResponse) {
// handle success
const noteStr = response.data.data;
if (isNote(noteStr)) {
createdNote = parseNote(noteStr);
}
})
.catch(function (error: AxiosError) {
// handle error
console.error(error.response?.status);
if (error.response?.status == 401) {
console.error("Creation of Note failed: unauthorized.");
err = "Creation of Note failed: unauthorized.";
} else if (error.response?.status == 500) {
console.error(
"Creation of Note failed: internal server error."
);
err = "Creation of Note failed: internal server error.";
} else {
console.log(error);
console.error(`Creation of new Note failed.`);
err = `Creation of new Note failed.`;
}
}
);

if (err)
throw err;

return createdNote;
};

export const updateNote = async (
id: Id,
user_id: Id,
coaching_session_id: Id,
body: string,
): Promise<Note> => {
const axios = require("axios");

var updatedNote: Note = defaultNote();
var err: string = "";

const newNoteJson = {
coaching_session_id: coaching_session_id,
user_id: user_id,
body: body
};

const data = await axios
.put(`http://localhost:4000/notes/${id}`, newNoteJson, {
withCredentials: true,
setTimeout: 5000, // 5 seconds before timing out trying to log in with the backend
headers: {
"X-Version": "0.0.1",
"Content-Type": "application/json",
},
})
.then(function (response: AxiosResponse) {
// handle success
if (isNote(response.data.data)) {
updatedNote = response.data.data;
}
})
.catch(function (error: AxiosError) {
// handle error
console.error(error.response?.status);
if (error.response?.status == 401) {
console.error("Update of Note failed: unauthorized.");
err = "Update of Organization failed: unauthorized.";
} else if (error.response?.status == 500) {
console.error("Update of Organization failed: internal server error.");
err = "Update of Organization failed: internal server error.";
} else {
console.log(error);
console.error(`Update of new Organization failed.`);
err = `Update of new Organization failed.`;
}
});

if (err)
throw err;

return updatedNote;
};
Loading

0 comments on commit e117c16

Please sign in to comment.