From 8994bf216f5ac470b17ae2235c28338b9f040ab4 Mon Sep 17 00:00:00 2001 From: Jim Hodapp Date: Fri, 30 Aug 2024 09:33:44 -0500 Subject: [PATCH 01/11] Rebase with main --- src/app/coaching-sessions/[id]/page.tsx | 5 +- src/app/login/page.tsx | 7 +- .../ui/coaching-sessions/agreements.tsx | 101 +++++++++++ src/lib/api/agreements.ts | 167 ++++++++++++++++++ src/types/agreement.ts | 93 ++++++++++ src/types/general.ts | 9 +- src/types/note.ts | 4 +- 7 files changed, 378 insertions(+), 8 deletions(-) create mode 100644 src/components/ui/coaching-sessions/agreements.tsx create mode 100644 src/lib/api/agreements.ts create mode 100644 src/types/agreement.ts diff --git a/src/app/coaching-sessions/[id]/page.tsx b/src/app/coaching-sessions/[id]/page.tsx index 3cdfd91..6eda0c1 100644 --- a/src/app/coaching-sessions/[id]/page.tsx +++ b/src/app/coaching-sessions/[id]/page.tsx @@ -48,6 +48,7 @@ import { import { Note, noteToString } from "@/types/note"; import { useAuthStore } from "@/lib/providers/auth-store-provider"; import { Id } from "@/types/general"; +import { Agreements } from "@/components/ui/coaching-sessions/agreements"; // export const metadata: Metadata = { // title: "Coaching Session", @@ -195,7 +196,9 @@ export default function CoachingSessionsPage() { Program -
Agreements
+
Actions
diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index c4527fe..6c9a881 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -5,6 +5,7 @@ import Link from "next/link"; import { cn } from "@/lib/utils"; import { buttonVariants } from "@/components/ui/button"; import { UserAuthForm } from "@/components/user-auth-form"; +import { siteConfig } from "@/site.config"; export const metadata: Metadata = { title: "Authentication", @@ -55,13 +56,11 @@ export default function AuthenticationPage() { > - Refactor Coaching & Mentoring + {siteConfig.name}
-

- A coaching and mentorship platform for engineering leaders and software engineers. -

+

{siteConfig.description}

diff --git a/src/components/ui/coaching-sessions/agreements.tsx b/src/components/ui/coaching-sessions/agreements.tsx new file mode 100644 index 0000000..ffcb686 --- /dev/null +++ b/src/components/ui/coaching-sessions/agreements.tsx @@ -0,0 +1,101 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { fetchAgreementsByCoachingSessionId } from "@/lib/api/agreements"; +import { Agreement } from "@/types/agreement"; +import { Id } from "@/types/general"; +import Link from "next/link"; +import { useEffect, useState } from "react"; +import { DateTime } from "ts-luxon"; + +export interface AgreementsProps { + /** The current active coaching session Id */ + coachingSessionId: Id; +} + +export function Agreements({ + coachingSessionId: coachingSessionId, + ...props +}: AgreementsProps) { + const [agreements, setAgreements] = useState([]); + + useEffect(() => { + async function loadAgreements() { + if (!coachingSessionId) return; + + await fetchAgreementsByCoachingSessionId(coachingSessionId) + .then((agreements) => { + // 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.debug("setAgreements: " + JSON.stringify(agreements)); + setAgreements(agreements); + }) + .catch(([err]) => { + console.error("Failed to fetch Agreements: " + err); + }); + } + loadAgreements(); + }, [coachingSessionId]); + + return ( +
+
+ {/* */} + +
+
+ +
+
+ +
+
+ ); +} diff --git a/src/lib/api/agreements.ts b/src/lib/api/agreements.ts new file mode 100644 index 0000000..557c828 --- /dev/null +++ b/src/lib/api/agreements.ts @@ -0,0 +1,167 @@ +// Interacts with the note endpoints + +import { Agreement, defaultAgreement, isAgreement, isAgreementArray, parseAgreement } from "@/types/agreement"; +import { Id } from "@/types/general"; +import { AxiosError, AxiosResponse } from "axios"; + +export const fetchAgreementsByCoachingSessionId = async ( + coachingSessionId: Id + ): Promise => { + const axios = require("axios"); + + var agreements: Agreement[] = []; + var err: string = ""; + + const data = await axios + .get(`http://localhost:4000/agreements`, { + 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 + var agreements_data = response.data.data; + if (isAgreementArray(agreements_data)) { + agreements_data.forEach((agreements_data: any) => { + agreements.push(parseAgreement(agreements_data)) + }); + } + }) + .catch(function (error: AxiosError) { + // handle error + if (error.response?.status == 401) { + console.error("Retrieval of Agreements failed: unauthorized."); + err = "Retrieval of Agreements failed: unauthorized."; + } else if (error.response?.status == 404) { + console.error("Retrieval of Agreements failed: Agreements by coaching session Id (" + coachingSessionId + ") not found."); + err = "Retrieval of Agreements failed: Agreements by coaching session Id (" + coachingSessionId + ") not found."; + } else { + console.error("GET error: " + error); + err = + `Retrieval of Agreements by coaching session Id (` + coachingSessionId + `) failed.`; + console.error(err); + } + }); + + if (err) + throw err; + + return agreements; + }; + +export const createAgreement = async ( + coaching_session_id: Id, + user_id: Id, + body: string + ): Promise => { + const axios = require("axios"); + + const newAgreementJson = { + coaching_session_id: coaching_session_id, + user_id: user_id, + body: body + }; + console.debug("newAgreementJson: " + JSON.stringify(newAgreementJson)); + // A full real note to be returned from the backend with the same body + var createdAgreement: Agreement = defaultAgreement(); + var err: string = ""; + + const data = await axios + .post(`http://localhost:4000/agreements`, newAgreementJson, { + 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 agreementStr = response.data.data; + if (isAgreement(agreementStr)) { + createdAgreement = parseAgreement(agreementStr); + } + }) + .catch(function (error: AxiosError) { + // handle error + console.error(error.response?.status); + if (error.response?.status == 401) { + console.error("Creation of Agreement failed: unauthorized."); + err = "Creation of Agreement failed: unauthorized."; + } else if (error.response?.status == 500) { + console.error( + "Creation of Agreement failed: internal server error." + ); + err = "Creation of Agreement failed: internal server error."; + } else { + console.log(error); + err = `Creation of new Agreement failed.`; + console.error(err); + } + } + ); + + if (err) + throw err; + + return createdAgreement; + }; + + export const updateAgreement = async ( + id: Id, + user_id: Id, + coaching_session_id: Id, + body: string, + ): Promise => { + const axios = require("axios"); + + var updatedAgreement: Agreement = defaultAgreement(); + var err: string = ""; + + const newAgreementJson = { + coaching_session_id: coaching_session_id, + user_id: user_id, + body: body + }; + + const data = await axios + .put(`http://localhost:4000/agreements/${id}`, newAgreementJson, { + 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 (isAgreement(response.data.data)) { + updatedAgreement = response.data.data; + } + }) + .catch(function (error: AxiosError) { + // handle error + console.error(error.response?.status); + if (error.response?.status == 401) { + err = "Update of Agreement failed: unauthorized."; + console.error(err); + } else if (error.response?.status == 500) { + err = "Update of Agreement failed: internal server error."; + console.error(err); + } else { + console.log(error); + err = `Update of new Agreement failed.`; + console.error(err); + } + }); + + if (err) + throw err; + + return updatedAgreement; + }; diff --git a/src/types/agreement.ts b/src/types/agreement.ts new file mode 100644 index 0000000..faee3ab --- /dev/null +++ b/src/types/agreement.ts @@ -0,0 +1,93 @@ +import { DateTime } from "ts-luxon"; +import { CompletionStatus, Id, SortOrder } from "@/types/general"; + +// This must always reflect the Rust struct on the backend +// entity::agreements::Model +export interface Agreement { + id: Id; + coaching_session_id: Id, + body?: string, + user_id: Id, + status: CompletionStatus, + status_changed_at?: DateTime, + created_at: DateTime; + updated_at: DateTime; +} + +// The main purpose of having this parsing function is to be able to parse the +// returned DateTimeWithTimeZone (Rust type) string into something that ts-luxon +// will agree to work with internally. +export function parseAgreement(data: any): Agreement { + if (!isAgreement(data)) { + throw new Error('Invalid Agreement object data'); + } + return { + id: data.id, + coaching_session_id: data.coaching_session_id, + body: data.body, + user_id: data.user_id, + status: data.status, + status_changed_at: data.status_changed_at, + created_at: DateTime.fromISO(data.created_at.toString()), + updated_at: DateTime.fromISO(data.updated_at.toString()), + }; +} + +export function isAgreement(value: unknown): value is Agreement { + if (!value || typeof value !== "object") { + return false; + } + const object = value as Record; + + return ( + (typeof object.id === "string" && + typeof object.coaching_session_id === "string" && + typeof object.user_id === "string" && + typeof object.status === "string" && + typeof object.created_at === "string" && + typeof object.updated_at === "string") || + (typeof object.body === "string" || // body is optional + typeof object.status_changed_at === "string") // status_changed_at is optional + ); + } + +export function isAgreementArray(value: unknown): value is Agreement[] { + return Array.isArray(value) && value.every(isAgreement); +} + +export function sortAgreementArray(agreements: Agreement[], order: SortOrder): Agreement[] { + if (order == SortOrder.Ascending) { + agreements.sort((a, b) => + new Date(a.updated_at.toString()).getTime() - new Date(b.updated_at.toString()).getTime()); + } else if (order == SortOrder.Descending) { + agreements.sort((a, b) => + new Date(b.updated_at.toString()).getTime() - new Date(a.updated_at.toString()).getTime()); + } + return agreements; +} + +export function defaultAgreement(): Agreement { + var now = DateTime.now(); + return { + id: "", + coaching_session_id: "", + body: "", + user_id: "", + status: CompletionStatus.NotStarted, + status_changed_at: now, + created_at: now, + updated_at: now, + }; + } + + export function defaultAgreements(): Agreement[] { + return [defaultAgreement()]; + } + + export function agreementToString(agreement: Agreement): string { + return JSON.stringify(agreement); + } + + export function agreementsToString(agreements: Agreement[]): string { + return JSON.stringify(agreements); + } \ No newline at end of file diff --git a/src/types/general.ts b/src/types/general.ts index 6771471..de69ecb 100644 --- a/src/types/general.ts +++ b/src/types/general.ts @@ -6,4 +6,11 @@ export type Id = string; export enum SortOrder { Ascending = "ascending", Descending = "descending" - } \ No newline at end of file + } + +export enum CompletionStatus { + NotStarted = "not_started", + InProgress = "in_progress", + Completed = "completed", + WontDo = "wont_do" +} \ No newline at end of file diff --git a/src/types/note.ts b/src/types/note.ts index 19f5ed3..df47124 100644 --- a/src/types/note.ts +++ b/src/types/note.ts @@ -17,7 +17,7 @@ export interface Note { // will agree to work with internally. export function parseNote(data: any): Note { if (!isNote(data)) { - throw new Error('Invalid Note data'); + throw new Error('Invalid Note object data'); } return { id: data.id, @@ -34,7 +34,7 @@ export function isNote(value: unknown): value is Note { return false; } const object = value as Record; - + return ( typeof object.id === "string" && typeof object.coaching_session_id === "string" && From 5dcbdf5e385807ef4758869b9010a70ce309264f Mon Sep 17 00:00:00 2001 From: Jim Hodapp Date: Mon, 26 Aug 2024 21:27:00 -0500 Subject: [PATCH 02/11] Update the application site.name and site.description strings --- src/site.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/site.config.ts b/src/site.config.ts index 9aa0022..8e59852 100644 --- a/src/site.config.ts +++ b/src/site.config.ts @@ -1,9 +1,9 @@ export const siteConfig = { - name: "Refactor Platform", + name: "Refactor Coaching & Mentoring", url: "https://refactorcoach.com", ogImage: "https://ui.shadcn.com/og.jpg", description: - "Beautifully designed components that you can copy and paste into your apps. Accessible. Customizable. Open Source.", + "A platform for software engineers and engineering leaders to level up their foundational skills.", links: { twitter: "https://twitter.com/shadcn", github: "https://github.com/shadcn-ui/ui", From 070efa364ddad8677c02041ff2fa84b6cd1624f0 Mon Sep 17 00:00:00 2001 From: Jim Hodapp Date: Fri, 30 Aug 2024 09:23:36 -0500 Subject: [PATCH 03/11] Adds a static table placeholder for Agreements list --- src/app/coaching-sessions/[id]/page.tsx | 1 + .../ui/coaching-sessions/agreements.tsx | 485 +++++++++++++++++- src/components/ui/table.tsx | 117 +++++ 3 files changed, 591 insertions(+), 12 deletions(-) create mode 100644 src/components/ui/table.tsx diff --git a/src/app/coaching-sessions/[id]/page.tsx b/src/app/coaching-sessions/[id]/page.tsx index 6eda0c1..be82736 100644 --- a/src/app/coaching-sessions/[id]/page.tsx +++ b/src/app/coaching-sessions/[id]/page.tsx @@ -49,6 +49,7 @@ import { Note, noteToString } from "@/types/note"; import { useAuthStore } from "@/lib/providers/auth-store-provider"; import { Id } from "@/types/general"; import { Agreements } from "@/components/ui/coaching-sessions/agreements"; +import { Agreement } from "@/types/agreement"; // export const metadata: Metadata = { // title: "Coaching Session", diff --git a/src/components/ui/coaching-sessions/agreements.tsx b/src/components/ui/coaching-sessions/agreements.tsx index ffcb686..fb11b4c 100644 --- a/src/components/ui/coaching-sessions/agreements.tsx +++ b/src/components/ui/coaching-sessions/agreements.tsx @@ -26,16 +26,65 @@ import Link from "next/link"; import { useEffect, useState } from "react"; import { DateTime } from "ts-luxon"; +import Image from "next/image"; +import { + File, + Home, + LineChart, + ListFilter, + MoreHorizontal, + Package, + Package2, + PanelLeft, + PlusCircle, + Search, + Settings, + ShoppingCart, + Users2, +} from "lucide-react"; + +import { Badge } from "@/components/ui/badge"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Input } from "@/components/ui/input"; +import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; + export interface AgreementsProps { /** The current active coaching session Id */ coachingSessionId: Id; } -export function Agreements({ - coachingSessionId: coachingSessionId, - ...props -}: AgreementsProps) { +const Agreements: React.FC<{ + coachingSessionId: Id; + // onChange: (value: string) => void; + // onKeyDown: () => void; +}> = ({ coachingSessionId /*, onChange, onKeyDown*/ }) => { + const WAIT_INTERVAL = 1000; + const [timer, setTimer] = useState(undefined); const [agreements, setAgreements] = useState([]); + const [agreementId, setAgreementId] = useState(""); + const [agreementBody, setAgreementBody] = useState(""); useEffect(() => { async function loadAgreements() { @@ -56,21 +105,358 @@ export function Agreements({ loadAgreements(); }, [coachingSessionId]); + const handleAgreementSelectionChange = (newValue: string) => { + console.debug("newValue (hASC): " + newValue); + setAgreementBody(newValue); + //setAgreementId(); + }; + + const handleAgreementChange = (e: React.ChangeEvent) => { + const newValue = e.target.value; + console.debug("newValue (hAC): " + newValue); + setAgreementBody(newValue); + + if (timer) { + clearTimeout(timer); + } + + const newTimer = window.setTimeout(() => { + //onChange(newValue); + }, WAIT_INTERVAL); + + setTimer(newTimer); + }; + + const handleOnKeyDown = (e: React.KeyboardEvent) => { + //onKeyDown(); + }; + + useEffect(() => { + return () => { + if (timer) { + clearTimeout(timer); + } + }; + }, [timer]); + return (
-
- {/* */} +
+
+
+ +
+ + All + Active + Draft + + Archived + + +
+ +
+
+ + + + Agreements + + Manage your agreements for this session + + + + + + + + Image + + Body + + Created + + + Last updated + + + Actions + + + + + + + + + + Laser Lemonade Machine + + + 2023-07-12 10:42 AM + + + 2024-02-12 9:42 AM + + + + + + + + Actions + Edit + Delete + + + + + + + + + + Hypernova Headphones + + + 2023-10-18 03:21 PM + + + 2024-10-18 03:21 PM + + + + + + + + Actions + Edit + Delete + + + + + + + + + + AeroGlow Desk Lamp + + + 2023-11-29 08:15 AM + + + 2024-11-29 08:15 AM + + + + + + + + Actions + Edit + Delete + + + + + + + + + + TechTonic Energy Drink + + + 2023-12-25 11:59 PM + + + 2024-12-25 11:59 PM + + + + + + + + Actions + Edit + Delete + + + + + + + + + + Gamer Gear Pro Controller + + + 2024-01-01 12:00 AM + + + 2024-05-01 12:00 AM + + + + + + + + Actions + Edit + Delete + + + + + + + + + + Luminous VR Headset + + + 2024-02-14 02:14 PM + + + 2024-04-14 02:14 PM + + + + + + + + Actions + Edit + Delete + + + + + +
+
+ +
+ Showing 1-10 of 32{" "} + products +
+
+
+
+
+
+
+
+ + {/* ------------------ */} + {/*
- +
-
+
*/}
); -} +}; + +export { Agreements }; + +// export function Agreements({ +// coachingSessionId: coachingSessionId, +// ...props +// }: AgreementsProps) { +// const [agreements, setAgreements] = useState([]); + +// useEffect(() => { +// async function loadAgreements() { +// if (!coachingSessionId) return; + +// await fetchAgreementsByCoachingSessionId(coachingSessionId) +// .then((agreements) => { +// // 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.debug("setAgreements: " + JSON.stringify(agreements)); +// setAgreements(agreements); +// }) +// .catch(([err]) => { +// console.error("Failed to fetch Agreements: " + err); +// }); +// } +// loadAgreements(); +// }, [coachingSessionId]); + +// return ( +//
+//
+// {/* */} +// +//
+//
+// +//
+//
+// +//
+//
+// ); +// } diff --git a/src/components/ui/table.tsx b/src/components/ui/table.tsx new file mode 100644 index 0000000..7f3502f --- /dev/null +++ b/src/components/ui/table.tsx @@ -0,0 +1,117 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)) +Table.displayName = "Table" + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = "TableHeader" + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = "TableBody" + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className + )} + {...props} + /> +)) +TableFooter.displayName = "TableFooter" + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableRow.displayName = "TableRow" + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableHead.displayName = "TableHead" + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableCell.displayName = "TableCell" + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableCaption.displayName = "TableCaption" + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} From ce0a13f85c24e0765bf67ae8277ece6faea50bf5 Mon Sep 17 00:00:00 2001 From: Jim Hodapp Date: Fri, 30 Aug 2024 09:23:55 -0500 Subject: [PATCH 04/11] Update site description --- src/site.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/site.config.ts b/src/site.config.ts index 8e59852..d4176e3 100644 --- a/src/site.config.ts +++ b/src/site.config.ts @@ -3,7 +3,7 @@ export const siteConfig = { url: "https://refactorcoach.com", ogImage: "https://ui.shadcn.com/og.jpg", description: - "A platform for software engineers and engineering leaders to level up their foundational skills.", + "A platform for software engineers and tech leaders to level up their foundational skills.", links: { twitter: "https://twitter.com/shadcn", github: "https://github.com/shadcn-ui/ui", From 9166e12343a654478c0a51584de6e5f8530299bb Mon Sep 17 00:00:00 2001 From: Jim Hodapp Date: Thu, 12 Sep 2024 21:47:25 -0500 Subject: [PATCH 05/11] List any set agreements in a table with rows --- src/app/coaching-sessions/[id]/page.tsx | 42 +- .../ui/coaching-sessions/agreements.tsx | 423 ++++-------------- 2 files changed, 103 insertions(+), 362 deletions(-) diff --git a/src/app/coaching-sessions/[id]/page.tsx b/src/app/coaching-sessions/[id]/page.tsx index be82736..11bd36b 100644 --- a/src/app/coaching-sessions/[id]/page.tsx +++ b/src/app/coaching-sessions/[id]/page.tsx @@ -190,30 +190,34 @@ export default function CoachingSessionsPage() {
- - - Agreements - Actions - Program - - - - - -
Actions
-
- -
Program
-
-
+
+ +
+ + Agreements + Actions + Program + +
+ + + + + {/*
Actions
*/} +
+ + {/*
Program
*/} +
+
+
-
+
diff --git a/src/components/ui/coaching-sessions/agreements.tsx b/src/components/ui/coaching-sessions/agreements.tsx index fb11b4c..02dbe05 100644 --- a/src/components/ui/coaching-sessions/agreements.tsx +++ b/src/components/ui/coaching-sessions/agreements.tsx @@ -140,352 +140,89 @@ const Agreements: React.FC<{ }, [timer]); return ( -
-
-
-
- -
- - All - Active - Draft - - Archived - - -
- -
+
+
+
+ + + Agreements + + Manage agreements for this session + + + + + + + Body + + Created + + + Last updated + + + Actions + + + + + {agreements.map((agreement) => ( + + + {agreement.body} + + + {agreement.created_at.toLocaleString( + DateTime.DATETIME_FULL + )} + + + {agreement.updated_at.toLocaleString( + DateTime.DATETIME_FULL + )} + + + + + + + + Actions + Edit + Delete + + + + + ))} + {agreements.length == 0 && ( + +
No agreements
+
+ )} +
+
+
+ +
+ Showing{" "} + + {agreements.length > 0 ? <>1 - : <>} {agreements.length} + {" "} + of {agreements.length} agreements
- - - - Agreements - - Manage your agreements for this session - - - - - - - - Image - - Body - - Created - - - Last updated - - - Actions - - - - - - - - - - Laser Lemonade Machine - - - 2023-07-12 10:42 AM - - - 2024-02-12 9:42 AM - - - - - - - - Actions - Edit - Delete - - - - - - - - - - Hypernova Headphones - - - 2023-10-18 03:21 PM - - - 2024-10-18 03:21 PM - - - - - - - - Actions - Edit - Delete - - - - - - - - - - AeroGlow Desk Lamp - - - 2023-11-29 08:15 AM - - - 2024-11-29 08:15 AM - - - - - - - - Actions - Edit - Delete - - - - - - - - - - TechTonic Energy Drink - - - 2023-12-25 11:59 PM - - - 2024-12-25 11:59 PM - - - - - - - - Actions - Edit - Delete - - - - - - - - - - Gamer Gear Pro Controller - - - 2024-01-01 12:00 AM - - - 2024-05-01 12:00 AM - - - - - - - - Actions - Edit - Delete - - - - - - - - - - Luminous VR Headset - - - 2024-02-14 02:14 PM - - - 2024-04-14 02:14 PM - - - - - - - - Actions - Edit - Delete - - - - - -
-
- -
- Showing 1-10 of 32{" "} - products -
-
-
-
- -
+ +
- - {/* ------------------ */} - {/*
- -
-
- -
-
- -
*/}
); }; From cf72c5bebb5486ed363bb0aa4e7b2e44c5f4ac98 Mon Sep 17 00:00:00 2001 From: Jim Hodapp Date: Fri, 20 Sep 2024 22:00:41 -0500 Subject: [PATCH 06/11] Adds a pretty solid agreements-list React component and uses it on the coaching session page. --- src/app/coaching-sessions/[id]/page.tsx | 13 +- .../ui/coaching-sessions/agreements-list.tsx | 234 ++++++++++++++++++ 2 files changed, 243 insertions(+), 4 deletions(-) create mode 100644 src/components/ui/coaching-sessions/agreements-list.tsx diff --git a/src/app/coaching-sessions/[id]/page.tsx b/src/app/coaching-sessions/[id]/page.tsx index 11bd36b..55a047c 100644 --- a/src/app/coaching-sessions/[id]/page.tsx +++ b/src/app/coaching-sessions/[id]/page.tsx @@ -48,7 +48,8 @@ import { import { Note, noteToString } from "@/types/note"; import { useAuthStore } from "@/lib/providers/auth-store-provider"; import { Id } from "@/types/general"; -import { Agreements } from "@/components/ui/coaching-sessions/agreements"; +//import { Agreements } from "@/components/ui/coaching-sessions/agreements"; +import { AgreementsList } from "@/components/ui/coaching-sessions/agreements-list"; import { Agreement } from "@/types/agreement"; // export const metadata: Metadata = { @@ -200,9 +201,13 @@ export default function CoachingSessionsPage() {
- + {/* TODO: see if I can add small, medium & large breakpoints for this div */} +
+ +
{/*
Actions
*/} diff --git a/src/components/ui/coaching-sessions/agreements-list.tsx b/src/components/ui/coaching-sessions/agreements-list.tsx new file mode 100644 index 0000000..dfca6a6 --- /dev/null +++ b/src/components/ui/coaching-sessions/agreements-list.tsx @@ -0,0 +1,234 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { MoreHorizontal, ArrowUpDown, Save } from "lucide-react"; +import { CompletionStatus, Id } from "@/types/general"; +import { fetchAgreementsByCoachingSessionId } from "@/lib/api/agreements"; +import { Agreement } from "@/types/agreement"; +import { DateTime } from "ts-luxon"; + +// interface Agreement { +// id: number; +// text: string; +// createdAt: string; +// lastUpdated: string; +// } + +const AgreementsList: React.FC<{ + coachingSessionId: Id; + userId: Id; +}> = ({ coachingSessionId, userId }) => { + const [agreements, setAgreements] = useState([]); + const [newAgreement, setNewAgreement] = useState(""); + const [editingId, setEditingId] = useState(null); + const [editBody, setEditBody] = useState(""); + const [sortColumn, setSortColumn] = useState("created_at"); + const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc"); + + const addAgreement = () => { + if (newAgreement.trim() === "") return; + const now = DateTime.now(); + setAgreements((prevAgreements) => [ + ...prevAgreements, + { + id: "", + coaching_session_id: coachingSessionId, + body: newAgreement, + user_id: userId, + status: CompletionStatus.NotStarted, + status_changed_at: now, + created_at: now, + updated_at: now, + }, + ]); + setNewAgreement(""); + }; + + const updateAgreement = (id: Id, newBody: string) => { + setAgreements( + agreements.map((agreement) => + agreement.id === id + ? { + ...agreement, + body: newBody, + updated_at: DateTime.now(), + } + : agreement + ) + ); + setEditingId(null); + setEditBody(""); + }; + + const deleteAgreement = (id: Id) => { + setAgreements(agreements.filter((agreement) => agreement.id !== id)); + }; + + const sortAgreements = (column: keyof Agreement) => { + if (column === sortColumn) { + setSortDirection(sortDirection === "asc" ? "desc" : "asc"); + } else { + setSortColumn(column); + setSortDirection("asc"); + } + }; + + const sortedAgreements = [...agreements].sort((a, b) => { + const aValue = a[sortColumn as keyof Agreement]!; + const bValue = b[sortColumn as keyof Agreement]!; + + if (aValue < bValue) return sortDirection === "asc" ? -1 : 1; + if (aValue > bValue) return sortDirection === "asc" ? 1 : -1; + return 0; + }); + + // const sortedAgreements = [...agreements].sort((a, b) => { + // if (a[sortColumn] < b[sortColumn]) return sortDirection === "asc" ? -1 : 1; + // if (a[sortColumn] > b[sortColumn]) return sortDirection === "asc" ? 1 : -1; + // return 0; + // }); + + useEffect(() => { + async function loadAgreements() { + if (!coachingSessionId) return; + + await fetchAgreementsByCoachingSessionId(coachingSessionId) + .then((agreements) => { + console.debug("setAgreements: " + JSON.stringify(agreements)); + setAgreements(agreements); + }) + .catch(([err]) => { + console.error("Failed to fetch Agreements: " + err); + }); + } + loadAgreements(); + }, [coachingSessionId]); + + return ( +
+
+
+ + + + sortAgreements("body")} + className="cursor-pointer" + > + Agreement + + sortAgreements("created_at")} + className="cursor-pointer hidden md:table-cell" + > + Created At + + sortAgreements("updated_at")} + className="cursor-pointer hidden md:table-cell" + > + Last Updated + + + + + + {sortedAgreements.map((agreement) => ( + + + {editingId === agreement.id ? ( +
+ setEditBody(e.target.value)} + onKeyPress={(e) => + e.key === "Enter" && + updateAgreement(agreement.id, editBody) + } + className="flex-grow" + /> + +
+ ) : ( + agreement.body + )} +
+ + {agreement.created_at.toLocaleString( + DateTime.DATETIME_FULL + )} + + + {agreement.updated_at.toLocaleString( + DateTime.DATETIME_FULL + )} + + + + + + + + { + setEditingId(agreement.id); + setEditBody(agreement.body ?? ""); + }} + > + Edit + + deleteAgreement(agreement.id)} + > + Delete + + + + +
+ ))} +
+
+
+
+ setNewAgreement(e.target.value)} + onKeyPress={(e) => e.key === "Enter" && addAgreement()} + placeholder="Enter new agreement" + className="flex-grow" + /> + +
+
+
+ ); +}; + +export { AgreementsList }; From 93db89b220fd865b1d6f1b868778bb1583a4699e Mon Sep 17 00:00:00 2001 From: Jim Hodapp Date: Fri, 20 Sep 2024 22:49:46 -0500 Subject: [PATCH 07/11] Remove status and status_changed for Agreements. Also can update an existing Agreement. --- .../ui/coaching-sessions/agreements-list.tsx | 101 ++++++++++-------- src/lib/api/agreements.ts | 32 +++--- src/types/agreement.ts | 10 +- 3 files changed, 74 insertions(+), 69 deletions(-) diff --git a/src/components/ui/coaching-sessions/agreements-list.tsx b/src/components/ui/coaching-sessions/agreements-list.tsx index dfca6a6..014ea6f 100644 --- a/src/components/ui/coaching-sessions/agreements-list.tsx +++ b/src/components/ui/coaching-sessions/agreements-list.tsx @@ -18,18 +18,15 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { MoreHorizontal, ArrowUpDown, Save } from "lucide-react"; -import { CompletionStatus, Id } from "@/types/general"; -import { fetchAgreementsByCoachingSessionId } from "@/lib/api/agreements"; -import { Agreement } from "@/types/agreement"; +import { Id } from "@/types/general"; +import { + createAgreement, + updateAgreement as updateAgreementApi, + fetchAgreementsByCoachingSessionId, +} from "@/lib/api/agreements"; +import { Agreement, agreementToString } from "@/types/agreement"; import { DateTime } from "ts-luxon"; -// interface Agreement { -// id: number; -// text: string; -// createdAt: string; -// lastUpdated: string; -// } - const AgreementsList: React.FC<{ coachingSessionId: Id; userId: Id; @@ -43,37 +40,63 @@ const AgreementsList: React.FC<{ const addAgreement = () => { if (newAgreement.trim() === "") return; - const now = DateTime.now(); - setAgreements((prevAgreements) => [ - ...prevAgreements, - { - id: "", - coaching_session_id: coachingSessionId, - body: newAgreement, - user_id: userId, - status: CompletionStatus.NotStarted, - status_changed_at: now, - created_at: now, - updated_at: now, - }, - ]); + + createAgreement(coachingSessionId, userId, newAgreement) + .then((agreement) => { + console.trace( + "Newly created Agreement: " + agreementToString(agreement) + ); + setAgreements((prevAgreements) => [ + ...prevAgreements, + { + id: agreement.id, + coaching_session_id: agreement.coaching_session_id, + body: agreement.body, + user_id: agreement.user_id, + created_at: agreement.created_at, + updated_at: agreement.updated_at, + }, + ]); + }) + .catch((err) => { + console.error("Failed to create new Agreement: " + err); + throw err; + }); + setNewAgreement(""); }; - const updateAgreement = (id: Id, newBody: string) => { - setAgreements( - agreements.map((agreement) => - agreement.id === id - ? { - ...agreement, + const updateAgreement = async (id: Id, newBody: string) => { + try { + const updatedAgreements = await Promise.all( + agreements.map(async (agreement) => { + if (agreement.id === id) { + // Call the async updateAgreement function + const updatedAgreement = await updateAgreementApi( + id, + agreement.user_id, + agreement.coaching_session_id, + newBody + ); + + return { + ...updatedAgreement, body: newBody, updated_at: DateTime.now(), - } - : agreement - ) - ); - setEditingId(null); - setEditBody(""); + }; + } + return agreement; + }) + ); + + setAgreements(updatedAgreements); + setEditingId(null); + setEditBody(""); + } catch (err) { + console.error("Failed to update Agreement:", err); + throw err; + // Handle error (e.g., show an error message to the user) + } }; const deleteAgreement = (id: Id) => { @@ -98,12 +121,6 @@ const AgreementsList: React.FC<{ return 0; }); - // const sortedAgreements = [...agreements].sort((a, b) => { - // if (a[sortColumn] < b[sortColumn]) return sortDirection === "asc" ? -1 : 1; - // if (a[sortColumn] > b[sortColumn]) return sortDirection === "asc" ? 1 : -1; - // return 0; - // }); - useEffect(() => { async function loadAgreements() { if (!coachingSessionId) return; diff --git a/src/lib/api/agreements.ts b/src/lib/api/agreements.ts index 557c828..e7fd574 100644 --- a/src/lib/api/agreements.ts +++ b/src/lib/api/agreements.ts @@ -1,8 +1,9 @@ // Interacts with the note endpoints import { Agreement, defaultAgreement, isAgreement, isAgreementArray, parseAgreement } from "@/types/agreement"; -import { Id } from "@/types/general"; +import { CompletionStatus, Id } from "@/types/general"; import { AxiosError, AxiosResponse } from "axios"; +import { DateTime } from "ts-luxon"; export const fetchAgreementsByCoachingSessionId = async ( coachingSessionId: Id @@ -35,21 +36,19 @@ export const fetchAgreementsByCoachingSessionId = async ( .catch(function (error: AxiosError) { // handle error if (error.response?.status == 401) { - console.error("Retrieval of Agreements failed: unauthorized."); err = "Retrieval of Agreements failed: unauthorized."; } else if (error.response?.status == 404) { - console.error("Retrieval of Agreements failed: Agreements by coaching session Id (" + coachingSessionId + ") not found."); err = "Retrieval of Agreements failed: Agreements by coaching session Id (" + coachingSessionId + ") not found."; } else { - console.error("GET error: " + error); err = `Retrieval of Agreements by coaching session Id (` + coachingSessionId + `) failed.`; - console.error(err); } }); - if (err) + if (err) { + console.error(err); throw err; + } return agreements; }; @@ -64,7 +63,9 @@ export const createAgreement = async ( const newAgreementJson = { coaching_session_id: coaching_session_id, user_id: user_id, - body: body + body: body, + status: CompletionStatus.NotStarted, + status_changed_at: DateTime.now() }; console.debug("newAgreementJson: " + JSON.stringify(newAgreementJson)); // A full real note to be returned from the backend with the same body @@ -91,23 +92,19 @@ export const createAgreement = async ( // handle error console.error(error.response?.status); if (error.response?.status == 401) { - console.error("Creation of Agreement failed: unauthorized."); err = "Creation of Agreement failed: unauthorized."; } else if (error.response?.status == 500) { - console.error( - "Creation of Agreement failed: internal server error." - ); err = "Creation of Agreement failed: internal server error."; } else { - console.log(error); err = `Creation of new Agreement failed.`; - console.error(err); } } ); - if (err) + if (err) { + console.error(err); throw err; + } return createdAgreement; }; @@ -149,19 +146,18 @@ export const createAgreement = async ( console.error(error.response?.status); if (error.response?.status == 401) { err = "Update of Agreement failed: unauthorized."; - console.error(err); } else if (error.response?.status == 500) { err = "Update of Agreement failed: internal server error."; - console.error(err); } else { console.log(error); err = `Update of new Agreement failed.`; - console.error(err); } }); - if (err) + if (err) { + console.error(err); throw err; + } return updatedAgreement; }; diff --git a/src/types/agreement.ts b/src/types/agreement.ts index faee3ab..2ec3980 100644 --- a/src/types/agreement.ts +++ b/src/types/agreement.ts @@ -8,8 +8,6 @@ export interface Agreement { coaching_session_id: Id, body?: string, user_id: Id, - status: CompletionStatus, - status_changed_at?: DateTime, created_at: DateTime; updated_at: DateTime; } @@ -26,8 +24,6 @@ export function parseAgreement(data: any): Agreement { coaching_session_id: data.coaching_session_id, body: data.body, user_id: data.user_id, - status: data.status, - status_changed_at: data.status_changed_at, created_at: DateTime.fromISO(data.created_at.toString()), updated_at: DateTime.fromISO(data.updated_at.toString()), }; @@ -43,11 +39,9 @@ export function isAgreement(value: unknown): value is Agreement { (typeof object.id === "string" && typeof object.coaching_session_id === "string" && typeof object.user_id === "string" && - typeof object.status === "string" && typeof object.created_at === "string" && typeof object.updated_at === "string") || - (typeof object.body === "string" || // body is optional - typeof object.status_changed_at === "string") // status_changed_at is optional + typeof object.body === "string" // body is optional ); } @@ -73,8 +67,6 @@ export function defaultAgreement(): Agreement { coaching_session_id: "", body: "", user_id: "", - status: CompletionStatus.NotStarted, - status_changed_at: now, created_at: now, updated_at: now, }; From 15a94dbb9bd936856f227bdf11d23f955253a757 Mon Sep 17 00:00:00 2001 From: Jim Hodapp Date: Sat, 21 Sep 2024 12:30:38 -0500 Subject: [PATCH 08/11] Deletion of an Agreement by its Id no works --- .../ui/coaching-sessions/agreements-list.tsx | 14 +++++- src/lib/api/agreements.ts | 44 ++++++++++++++++++- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/components/ui/coaching-sessions/agreements-list.tsx b/src/components/ui/coaching-sessions/agreements-list.tsx index 014ea6f..192fc76 100644 --- a/src/components/ui/coaching-sessions/agreements-list.tsx +++ b/src/components/ui/coaching-sessions/agreements-list.tsx @@ -21,6 +21,7 @@ import { MoreHorizontal, ArrowUpDown, Save } from "lucide-react"; import { Id } from "@/types/general"; import { createAgreement, + deleteAgreement as deleteAgreementApi, updateAgreement as updateAgreementApi, fetchAgreementsByCoachingSessionId, } from "@/lib/api/agreements"; @@ -41,6 +42,8 @@ const AgreementsList: React.FC<{ const addAgreement = () => { if (newAgreement.trim() === "") return; + // TODO: move this and the other backend calls outside of this component and trigger + // an event instead, especially if we end up making this a reusable Agreement/Action component. createAgreement(coachingSessionId, userId, newAgreement) .then((agreement) => { console.trace( @@ -100,7 +103,16 @@ const AgreementsList: React.FC<{ }; const deleteAgreement = (id: Id) => { - setAgreements(agreements.filter((agreement) => agreement.id !== id)); + deleteAgreementApi(id) + .then((deleted_id) => { + console.trace("Deleted Agreement id: " + JSON.stringify(deleted_id)); + + setAgreements(agreements.filter((agreement) => agreement.id !== id)); + }) + .catch((err) => { + console.error("Failed to create new Agreement: " + err); + throw err; + }); }; const sortAgreements = (column: keyof Agreement) => { diff --git a/src/lib/api/agreements.ts b/src/lib/api/agreements.ts index e7fd574..3d4ccb8 100644 --- a/src/lib/api/agreements.ts +++ b/src/lib/api/agreements.ts @@ -149,7 +149,6 @@ export const createAgreement = async ( } else if (error.response?.status == 500) { err = "Update of Agreement failed: internal server error."; } else { - console.log(error); err = `Update of new Agreement failed.`; } }); @@ -161,3 +160,46 @@ export const createAgreement = async ( return updatedAgreement; }; + + export const deleteAgreement = async ( + id: Id + ): Promise => { + const axios = require("axios"); + + var deletedAgreement: Agreement = defaultAgreement(); + var err: string = ""; + + const data = await axios + .delete(`http://localhost:4000/agreements/${id}`, { + 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 (isAgreement(response.data.data)) { + deletedAgreement = response.data.data; + } + }) + .catch(function (error: AxiosError) { + // handle error + console.error(error.response?.status); + if (error.response?.status == 401) { + err = "Deletion of Agreement failed: unauthorized."; + } else if (error.response?.status == 500) { + err = "Deletion of Agreement failed: internal server error."; + } else { + err = `Deletion of new Agreement failed.`; + } + }); + + if (err) { + console.error(err); + throw err; + } + + return deletedAgreement; + }; From 1afdfcea30d799661a6e1b65ff3cd70c9aad8050 Mon Sep 17 00:00:00 2001 From: Jim Hodapp Date: Sun, 22 Sep 2024 11:41:12 -0500 Subject: [PATCH 09/11] Remove old attempt at an agreements component. --- .../ui/coaching-sessions/agreements.tsx | 299 ------------------ 1 file changed, 299 deletions(-) delete mode 100644 src/components/ui/coaching-sessions/agreements.tsx diff --git a/src/components/ui/coaching-sessions/agreements.tsx b/src/components/ui/coaching-sessions/agreements.tsx deleted file mode 100644 index 02dbe05..0000000 --- a/src/components/ui/coaching-sessions/agreements.tsx +++ /dev/null @@ -1,299 +0,0 @@ -"use client"; - -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { Label } from "@/components/ui/label"; -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectLabel, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { fetchAgreementsByCoachingSessionId } from "@/lib/api/agreements"; -import { Agreement } from "@/types/agreement"; -import { Id } from "@/types/general"; -import Link from "next/link"; -import { useEffect, useState } from "react"; -import { DateTime } from "ts-luxon"; - -import Image from "next/image"; -import { - File, - Home, - LineChart, - ListFilter, - MoreHorizontal, - Package, - Package2, - PanelLeft, - PlusCircle, - Search, - Settings, - ShoppingCart, - Users2, -} from "lucide-react"; - -import { Badge } from "@/components/ui/badge"; -import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { Input } from "@/components/ui/input"; -import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; - -export interface AgreementsProps { - /** The current active coaching session Id */ - coachingSessionId: Id; -} - -const Agreements: React.FC<{ - coachingSessionId: Id; - // onChange: (value: string) => void; - // onKeyDown: () => void; -}> = ({ coachingSessionId /*, onChange, onKeyDown*/ }) => { - const WAIT_INTERVAL = 1000; - const [timer, setTimer] = useState(undefined); - const [agreements, setAgreements] = useState([]); - const [agreementId, setAgreementId] = useState(""); - const [agreementBody, setAgreementBody] = useState(""); - - useEffect(() => { - async function loadAgreements() { - if (!coachingSessionId) return; - - await fetchAgreementsByCoachingSessionId(coachingSessionId) - .then((agreements) => { - // 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.debug("setAgreements: " + JSON.stringify(agreements)); - setAgreements(agreements); - }) - .catch(([err]) => { - console.error("Failed to fetch Agreements: " + err); - }); - } - loadAgreements(); - }, [coachingSessionId]); - - const handleAgreementSelectionChange = (newValue: string) => { - console.debug("newValue (hASC): " + newValue); - setAgreementBody(newValue); - //setAgreementId(); - }; - - const handleAgreementChange = (e: React.ChangeEvent) => { - const newValue = e.target.value; - console.debug("newValue (hAC): " + newValue); - setAgreementBody(newValue); - - if (timer) { - clearTimeout(timer); - } - - const newTimer = window.setTimeout(() => { - //onChange(newValue); - }, WAIT_INTERVAL); - - setTimer(newTimer); - }; - - const handleOnKeyDown = (e: React.KeyboardEvent) => { - //onKeyDown(); - }; - - useEffect(() => { - return () => { - if (timer) { - clearTimeout(timer); - } - }; - }, [timer]); - - return ( -
-
-
- - - Agreements - - Manage agreements for this session - - - - - - - Body - - Created - - - Last updated - - - Actions - - - - - {agreements.map((agreement) => ( - - - {agreement.body} - - - {agreement.created_at.toLocaleString( - DateTime.DATETIME_FULL - )} - - - {agreement.updated_at.toLocaleString( - DateTime.DATETIME_FULL - )} - - - - - - - - Actions - Edit - Delete - - - - - ))} - {agreements.length == 0 && ( - -
No agreements
-
- )} -
-
-
- -
- Showing{" "} - - {agreements.length > 0 ? <>1 - : <>} {agreements.length} - {" "} - of {agreements.length} agreements -
-
-
-
-
-
- ); -}; - -export { Agreements }; - -// export function Agreements({ -// coachingSessionId: coachingSessionId, -// ...props -// }: AgreementsProps) { -// const [agreements, setAgreements] = useState([]); - -// useEffect(() => { -// async function loadAgreements() { -// if (!coachingSessionId) return; - -// await fetchAgreementsByCoachingSessionId(coachingSessionId) -// .then((agreements) => { -// // 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.debug("setAgreements: " + JSON.stringify(agreements)); -// setAgreements(agreements); -// }) -// .catch(([err]) => { -// console.error("Failed to fetch Agreements: " + err); -// }); -// } -// loadAgreements(); -// }, [coachingSessionId]); - -// return ( -//
-//
-// {/* */} -// -//
-//
-// -//
-//
-// -//
-//
-// ); -// } From 8a68b4f44705ec747c6df8b127e227897b6cb0ab Mon Sep 17 00:00:00 2001 From: Jim Hodapp Date: Sun, 22 Sep 2024 20:30:03 -0500 Subject: [PATCH 10/11] Agreements are now fully working in an optimized modular architecture between the page and the component. --- src/app/coaching-sessions/[id]/page.tsx | 47 +++++- .../ui/coaching-sessions/agreements-list.tsx | 136 +++++++++++------- src/lib/api/agreements.ts | 9 +- src/site.config.ts | 1 + 4 files changed, 137 insertions(+), 56 deletions(-) diff --git a/src/app/coaching-sessions/[id]/page.tsx b/src/app/coaching-sessions/[id]/page.tsx index 55a047c..17c5997 100644 --- a/src/app/coaching-sessions/[id]/page.tsx +++ b/src/app/coaching-sessions/[id]/page.tsx @@ -48,9 +48,14 @@ import { import { Note, noteToString } from "@/types/note"; import { useAuthStore } from "@/lib/providers/auth-store-provider"; import { Id } from "@/types/general"; -//import { Agreements } from "@/components/ui/coaching-sessions/agreements"; import { AgreementsList } from "@/components/ui/coaching-sessions/agreements-list"; -import { Agreement } from "@/types/agreement"; +import { Agreement, agreementToString } from "@/types/agreement"; +import { + createAgreement, + deleteAgreement, + updateAgreement, +} from "@/lib/api/agreements"; +import { siteConfig } from "@/site.config"; // export const metadata: Metadata = { // title: "Coaching Session", @@ -94,6 +99,40 @@ export default function CoachingSessionsPage() { fetchNote(); }, [coachingSessionId, noteId]); + const handleAgreementAdded = (body: string): Promise => { + // Calls the backend endpoint that creates and stores a full Agreement entity + return createAgreement(coachingSessionId, userId, body) + .then((agreement) => { + return agreement; + }) + .catch((err) => { + console.error("Failed to create new Agreement: " + err); + throw err; + }); + }; + + const handleAgreementEdited = (id: Id, body: string): Promise => { + return updateAgreement(id, coachingSessionId, userId, body) + .then((agreement) => { + return agreement; + }) + .catch((err) => { + console.error("Failed to update Agreement (id: " + id + "): " + err); + throw err; + }); + }; + + const handleAgreementDeleted = (id: Id): Promise => { + return deleteAgreement(id) + .then((agreement) => { + return agreement; + }) + .catch((err) => { + console.error("Failed to update Agreement (id: " + id + "): " + err); + throw err; + }); + }; + const handleInputChange = (value: string) => { setNote(value); @@ -206,6 +245,10 @@ export default function CoachingSessionsPage() {
diff --git a/src/components/ui/coaching-sessions/agreements-list.tsx b/src/components/ui/coaching-sessions/agreements-list.tsx index 192fc76..94897b5 100644 --- a/src/components/ui/coaching-sessions/agreements-list.tsx +++ b/src/components/ui/coaching-sessions/agreements-list.tsx @@ -27,39 +27,50 @@ import { } from "@/lib/api/agreements"; import { Agreement, agreementToString } from "@/types/agreement"; import { DateTime } from "ts-luxon"; +import { siteConfig } from "@/site.config"; const AgreementsList: React.FC<{ coachingSessionId: Id; userId: Id; -}> = ({ coachingSessionId, userId }) => { + locale: string | "us"; + onAgreementAdded: (value: string) => Promise; + onAgreementEdited: (id: Id, value: string) => Promise; + onAgreementDeleted: (id: Id) => Promise; +}> = ({ + coachingSessionId, + userId, + locale, + onAgreementAdded, + onAgreementEdited, + onAgreementDeleted, +}) => { + enum AgreementSortField { + Body = "body", + CreatedAt = "created_at", + UpdatedAt = "updated_at", + } + const [agreements, setAgreements] = useState([]); const [newAgreement, setNewAgreement] = useState(""); const [editingId, setEditingId] = useState(null); const [editBody, setEditBody] = useState(""); - const [sortColumn, setSortColumn] = useState("created_at"); + const [sortColumn, setSortColumn] = useState( + AgreementSortField.CreatedAt + ); const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc"); const addAgreement = () => { if (newAgreement.trim() === "") return; - // TODO: move this and the other backend calls outside of this component and trigger - // an event instead, especially if we end up making this a reusable Agreement/Action component. - createAgreement(coachingSessionId, userId, newAgreement) + // Call the external onAgreementAdded handler function which should + // store this agreement in the backend database + onAgreementAdded(newAgreement) .then((agreement) => { console.trace( - "Newly created Agreement: " + agreementToString(agreement) + "Newly created Agreement (onAgreementAdded): " + + agreementToString(agreement) ); - setAgreements((prevAgreements) => [ - ...prevAgreements, - { - id: agreement.id, - coaching_session_id: agreement.coaching_session_id, - body: agreement.body, - user_id: agreement.user_id, - created_at: agreement.created_at, - updated_at: agreement.updated_at, - }, - ]); + setAgreements((prevAgreements) => [...prevAgreements, agreement]); }) .catch((err) => { console.error("Failed to create new Agreement: " + err); @@ -70,23 +81,30 @@ const AgreementsList: React.FC<{ }; const updateAgreement = async (id: Id, newBody: string) => { + const body = newBody.trim(); + if (body === "") return; + try { const updatedAgreements = await Promise.all( agreements.map(async (agreement) => { if (agreement.id === id) { - // Call the async updateAgreement function - const updatedAgreement = await updateAgreementApi( - id, - agreement.user_id, - agreement.coaching_session_id, - newBody - ); + // Call the external onAgreementEdited handler function which should + // update the stored version of this agreement in the backend database + agreement = await onAgreementEdited(id, body) + .then((updatedAgreement) => { + console.trace( + "Updated Agreement (onAgreementUpdated): " + + agreementToString(updatedAgreement) + ); - return { - ...updatedAgreement, - body: newBody, - updated_at: DateTime.now(), - }; + return updatedAgreement; + }) + .catch((err) => { + console.error( + "Failed to update Agreement (id: " + id + "): " + err + ); + throw err; + }); } return agreement; }) @@ -96,21 +114,26 @@ const AgreementsList: React.FC<{ setEditingId(null); setEditBody(""); } catch (err) { - console.error("Failed to update Agreement:", err); + console.error("Failed to update Agreement (id: " + id + "): ", err); throw err; - // Handle error (e.g., show an error message to the user) } }; const deleteAgreement = (id: Id) => { - deleteAgreementApi(id) - .then((deleted_id) => { - console.trace("Deleted Agreement id: " + JSON.stringify(deleted_id)); + if (id === "") return; + // Call the external onAgreementDeleted handler function which should + // delete this agreement from the backend database + onAgreementDeleted(id) + .then((agreement) => { + console.trace( + "Deleted Agreement (onAgreementDeleted): " + + agreementToString(agreement) + ); setAgreements(agreements.filter((agreement) => agreement.id !== id)); }) .catch((err) => { - console.error("Failed to create new Agreement: " + err); + console.error("Failed to Agreement (id: " + id + "): " + err); throw err; }); }; @@ -150,29 +173,42 @@ const AgreementsList: React.FC<{ }, [coachingSessionId]); return ( -
+
sortAgreements("body")} - className="cursor-pointer" + onClick={() => sortAgreements(AgreementSortField.Body)} + className={`cursor-pointer ${ + sortColumn === AgreementSortField.Body + ? "underline" + : "no-underline" + }`} > Agreement sortAgreements("created_at")} - className="cursor-pointer hidden md:table-cell" + className={`cursor-pointer hidden sm:table-cell ${ + sortColumn === AgreementSortField.CreatedAt + ? "underline" + : "no-underline" + }`} > - Created At + Created + sortAgreements("updated_at")} - className="cursor-pointer hidden md:table-cell" + className={`cursor-pointer hidden md:table-cell ${ + sortColumn === AgreementSortField.UpdatedAt + ? "underline" + : "no-underline" + }`} + onClick={() => sortAgreements(AgreementSortField.UpdatedAt)} > - Last Updated + Updated @@ -205,15 +241,15 @@ const AgreementsList: React.FC<{ agreement.body )} - - {agreement.created_at.toLocaleString( - DateTime.DATETIME_FULL - )} + + {agreement.created_at + .setLocale(siteConfig.locale) + .toLocaleString(DateTime.DATETIME_MED)} - {agreement.updated_at.toLocaleString( - DateTime.DATETIME_FULL - )} + {agreement.updated_at + .setLocale(siteConfig.locale) + .toLocaleString(DateTime.DATETIME_MED)} diff --git a/src/lib/api/agreements.ts b/src/lib/api/agreements.ts index 3d4ccb8..96d93de 100644 --- a/src/lib/api/agreements.ts +++ b/src/lib/api/agreements.ts @@ -1,4 +1,4 @@ -// Interacts with the note endpoints +// Interacts with the agreement endpoints import { Agreement, defaultAgreement, isAgreement, isAgreementArray, parseAgreement } from "@/types/agreement"; import { CompletionStatus, Id } from "@/types/general"; @@ -136,9 +136,10 @@ export const createAgreement = async ( }, }) .then(function (response: AxiosResponse) { - // handle success - if (isAgreement(response.data.data)) { - updatedAgreement = response.data.data; + // handle success + const agreement_data = response.data.data; + if (isAgreement(agreement_data)) { + updatedAgreement = parseAgreement(agreement_data); } }) .catch(function (error: AxiosError) { diff --git a/src/site.config.ts b/src/site.config.ts index d4176e3..be3c703 100644 --- a/src/site.config.ts +++ b/src/site.config.ts @@ -2,6 +2,7 @@ export const siteConfig = { name: "Refactor Coaching & Mentoring", url: "https://refactorcoach.com", ogImage: "https://ui.shadcn.com/og.jpg", + locale: "us", description: "A platform for software engineers and tech leaders to level up their foundational skills.", links: { From b6aa2fe9ff4071e621126c6de18020f097d217d6 Mon Sep 17 00:00:00 2001 From: Jim Hodapp Date: Sun, 22 Sep 2024 20:36:21 -0500 Subject: [PATCH 11/11] Remove unnecessary comment --- src/app/coaching-sessions/[id]/page.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/coaching-sessions/[id]/page.tsx b/src/app/coaching-sessions/[id]/page.tsx index 17c5997..dca1075 100644 --- a/src/app/coaching-sessions/[id]/page.tsx +++ b/src/app/coaching-sessions/[id]/page.tsx @@ -240,7 +240,6 @@ export default function CoachingSessionsPage() { - {/* TODO: see if I can add small, medium & large breakpoints for this div */}