From 6f0be1aabe8d8fa05f3310d5832a6c562fa019a2 Mon Sep 17 00:00:00 2001 From: Caleb Bourg Date: Wed, 2 Apr 2025 06:57:56 -0400 Subject: [PATCH 01/19] add the ability to update a coaching session --- src/components/ui/coaching-session.tsx | 81 +++++++--- .../dashboard/add-coaching-session-dialog.tsx | 130 --------------- src/components/ui/dashboard/add-entities.tsx | 7 +- .../ui/dashboard/coaching-session-dialog.tsx | 150 ++++++++++++++++++ .../ui/dashboard/coaching-session-list.tsx | 7 +- 5 files changed, 219 insertions(+), 156 deletions(-) delete mode 100644 src/components/ui/dashboard/add-coaching-session-dialog.tsx create mode 100644 src/components/ui/dashboard/coaching-session-dialog.tsx diff --git a/src/components/ui/coaching-session.tsx b/src/components/ui/coaching-session.tsx index d0e77880..41d6b2e0 100644 --- a/src/components/ui/coaching-session.tsx +++ b/src/components/ui/coaching-session.tsx @@ -1,6 +1,6 @@ "use client"; -import React from "react"; +import React, { useState } from "react"; import { format } from "date-fns"; import { Card, CardHeader } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; @@ -8,11 +8,23 @@ import Link from "next/link"; import { useCoachingSessionStateStore } from "@/lib/providers/coaching-session-state-store-provider"; import { useOverarchingGoalBySession } from "@/lib/api/overarching-goals"; import { Id } from "@/types/general"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { MoreHorizontal } from "lucide-react"; +import { CoachingSessionDialog } from "@/components/ui/dashboard/coaching-session-dialog"; +import { DateTime } from "ts-luxon"; interface CoachingSessionProps { coachingSession: { id: Id; date: string; + coaching_relationship_id: Id; + created_at: DateTime; + updated_at: DateTime; }; } @@ -22,29 +34,58 @@ const CoachingSession: React.FC = ({ const { setCurrentCoachingSessionId } = useCoachingSessionStateStore( (state) => state ); + const [updateDialogOpen, setUpdateDialogOpen] = useState(false); return ( - - -
-
- -
- {format(new Date(coachingSession.date), "MMMM d, yyyy h:mm a")} + <> + + +
+
+ +
+ {format(new Date(coachingSession.date), "MMMM d, yyyy h:mm a")} +
+ + + + + + + + setCurrentCoachingSessionId(coachingSession.id) + } + > + Join Session + + + setUpdateDialogOpen(true)}> + Update Session + + + Delete Session + + +
- - - -
- - + + + { + // Refresh the list of coaching sessions + window.location.reload(); + }} + existingSession={coachingSession} + mode="update" + /> + ); }; diff --git a/src/components/ui/dashboard/add-coaching-session-dialog.tsx b/src/components/ui/dashboard/add-coaching-session-dialog.tsx deleted file mode 100644 index 3cbcad19..00000000 --- a/src/components/ui/dashboard/add-coaching-session-dialog.tsx +++ /dev/null @@ -1,130 +0,0 @@ -"use client"; - -import React from "react"; -import { useState } from "react"; -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { Label } from "@/components/ui/label"; -import { Calendar } from "@/components/ui/calendar"; -import { useCoachingRelationshipStateStore } from "@/lib/providers/coaching-relationship-state-store-provider"; -import { getDateTimeFromString } from "@/types/general"; -import { - CoachingSession, - defaultCoachingSession, -} from "@/types/coaching-session"; -import { - useCoachingSessionList, - useCoachingSessionMutation, -} from "@/lib/api/coaching-sessions"; -import { DateTime } from "ts-luxon"; -import { useAuthStore } from "@/lib/providers/auth-store-provider"; -import { cn } from "@/components/lib/utils"; - -interface AddCoachingSessionDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - onCoachingSessionAdded: () => void; - dialogTrigger: React.ReactElement>; -} - -export function AddCoachingSessionDialog({ - open, - onOpenChange, - onCoachingSessionAdded, - dialogTrigger, -}: AddCoachingSessionDialogProps) { - const { currentCoachingRelationshipId } = useCoachingRelationshipStateStore( - (state) => state - ); - const fromDate = DateTime.now().minus({ month: 1 }); - const toDate = DateTime.now().plus({ month: 1 }); - const { refresh } = useCoachingSessionList( - currentCoachingRelationshipId, - fromDate, - toDate - ); - const { create: createCoachingSession } = useCoachingSessionMutation(); - const [newSessionDate, setNewSessionDate] = useState( - undefined - ); - const [newSessionTime, setNewSessionTime] = useState(""); - const { isCoach } = useAuthStore((state) => state); - - const handleCreateSession = (e: React.FormEvent) => { - e.preventDefault(); - if (!newSessionDate || !newSessionTime) return; - - const [hours, minutes] = newSessionTime.split(":").map(Number); - const dateTime = getDateTimeFromString(newSessionDate.toISOString()) - .set({ hour: hours, minute: minutes }) - .toFormat("yyyy-MM-dd'T'HH:mm:ss"); - - const newCoachingSession: CoachingSession = { - ...defaultCoachingSession(), - coaching_relationship_id: currentCoachingRelationshipId, - date: dateTime, - }; - - createCoachingSession(newCoachingSession) - .then(() => { - refresh(); - onCoachingSessionAdded(); - setNewSessionDate(undefined); - setNewSessionTime(""); - }) - .catch((err: Error) => { - console.error("Failed to create new Coaching Session: " + err); - throw err; - }); - }; - - return ( - - - {React.cloneElement(dialogTrigger, { - ...(dialogTrigger.props as React.HTMLAttributes), - className: cn( - (dialogTrigger.props as React.HTMLAttributes) - .className - ), - })} - - - - - Create New Coaching Session - -
-
- - setNewSessionDate(date)} - /> -
-
- - setNewSessionTime(e.target.value)} - className="w-full border rounded p-2" - required - /> -
- -
-
-
- ); -} diff --git a/src/components/ui/dashboard/add-entities.tsx b/src/components/ui/dashboard/add-entities.tsx index 76c83147..d2765f19 100644 --- a/src/components/ui/dashboard/add-entities.tsx +++ b/src/components/ui/dashboard/add-entities.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; -import { AddCoachingSessionDialog } from "./add-coaching-session-dialog"; +import { CoachingSessionDialog } from "./coaching-session-dialog"; import { AddCoachingSessionButton } from "./add-coaching-session-button"; import { AddMemberButton } from "./add-member-button"; import { useRouter } from "next/navigation"; @@ -28,10 +28,11 @@ export default function AddEntities() { Add New
- void; + onCoachingSessionUpdated: () => void; + dialogTrigger?: React.ReactElement>; + existingSession?: CoachingSession; + mode: "create" | "update"; +} + +export function CoachingSessionDialog({ + open, + onOpenChange, + onCoachingSessionUpdated, + dialogTrigger, + existingSession, + mode, +}: CoachingSessionDialogProps) { + const { currentCoachingRelationshipId } = useCoachingRelationshipStateStore( + (state) => state + ); + const fromDate = DateTime.now().minus({ month: 1 }); + const toDate = DateTime.now().plus({ month: 1 }); + const { refresh } = useCoachingSessionList( + currentCoachingRelationshipId, + fromDate, + toDate + ); + const { create: createCoachingSession, update } = + useCoachingSessionMutation(); + const [sessionDate, setSessionDate] = useState( + existingSession ? new Date(existingSession.date) : undefined + ); + const [sessionTime, setSessionTime] = useState( + existingSession ? format(new Date(existingSession.date), "HH:mm") : "" + ); + const { isCoach } = useAuthStore((state) => state); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!sessionDate || !sessionTime) return; + + const [hours, minutes] = sessionTime.split(":").map(Number); + const dateTime = getDateTimeFromString(sessionDate.toISOString()) + .set({ hour: hours, minute: minutes }) + .toFormat("yyyy-MM-dd'T'HH:mm:ss"); + + try { + if (mode === "create") { + const newCoachingSession: CoachingSession = { + ...defaultCoachingSession(), + coaching_relationship_id: currentCoachingRelationshipId, + date: dateTime, + }; + await createCoachingSession(newCoachingSession); + } else if (existingSession) { + await update(existingSession.id, { + ...existingSession, + date: dateTime, + updated_at: DateTime.now(), + }); + } + refresh(); + onCoachingSessionUpdated(); + setSessionDate(undefined); + setSessionTime(""); + onOpenChange(false); + } catch (error) { + console.error(`Failed to ${mode} coaching session:`, error); + } + }; + + return ( + + {dialogTrigger && ( + + {React.cloneElement(dialogTrigger, { + ...(dialogTrigger.props as React.HTMLAttributes), + className: cn( + (dialogTrigger.props as React.HTMLAttributes) + .className + ), + })} + + )} + + + + + {mode === "create" + ? "Create New Coaching Session" + : "Update Coaching Session"} + + +
+
+ + setSessionDate(date)} + /> +
+
+ + setSessionTime(e.target.value)} + className="w-full border rounded p-2" + required + /> +
+ +
+
+
+ ); +} diff --git a/src/components/ui/dashboard/coaching-session-list.tsx b/src/components/ui/dashboard/coaching-session-list.tsx index 2b366939..d8336ced 100644 --- a/src/components/ui/dashboard/coaching-session-list.tsx +++ b/src/components/ui/dashboard/coaching-session-list.tsx @@ -9,7 +9,7 @@ import { useCoachingRelationshipStateStore } from "@/lib/providers/coaching-rela import { useCoachingSessionList } from "@/lib/api/coaching-sessions"; import { CoachingSession as CoachingSessionComponent } from "@/components/ui/coaching-session"; import { DateTime } from "ts-luxon"; -import { AddCoachingSessionDialog } from "./add-coaching-session-dialog"; +import { CoachingSessionDialog } from "./coaching-session-dialog"; export default function CoachingSessionList() { const { currentCoachingRelationshipId } = useCoachingRelationshipStateStore( @@ -54,10 +54,11 @@ export default function CoachingSessionList() { Coaching Sessions - Date: Fri, 4 Apr 2025 07:59:50 -0400 Subject: [PATCH 02/19] separate form from dialog --- src/components/ui/coaching-session.tsx | 17 ++- src/components/ui/dashboard/add-entities.tsx | 38 +++--- .../ui/dashboard/coaching-session-dialog.tsx | 127 +++--------------- .../ui/dashboard/coaching-session-form.tsx | 111 +++++++++++++++ .../ui/dashboard/coaching-session-list.tsx | 36 ++--- 5 files changed, 168 insertions(+), 161 deletions(-) create mode 100644 src/components/ui/dashboard/coaching-session-form.tsx diff --git a/src/components/ui/coaching-session.tsx b/src/components/ui/coaching-session.tsx index 41d6b2e0..368314dc 100644 --- a/src/components/ui/coaching-session.tsx +++ b/src/components/ui/coaching-session.tsx @@ -16,6 +16,7 @@ import { } from "@/components/ui/dropdown-menu"; import { MoreHorizontal } from "lucide-react"; import { CoachingSessionDialog } from "@/components/ui/dashboard/coaching-session-dialog"; +import CoachingSessionForm, { CoachingSessionFormMode } from "@/components/ui/dashboard/coaching-session-form"; import { DateTime } from "ts-luxon"; interface CoachingSessionProps { @@ -35,6 +36,7 @@ const CoachingSession: React.FC = ({ (state) => state ); const [updateDialogOpen, setUpdateDialogOpen] = useState(false); + const mode: CoachingSessionFormMode = "update"; return ( <> @@ -78,13 +80,14 @@ const CoachingSession: React.FC = ({ { - // Refresh the list of coaching sessions - window.location.reload(); - }} - existingSession={coachingSession} - mode="update" - /> + mode={mode} + > + + ); }; diff --git a/src/components/ui/dashboard/add-entities.tsx b/src/components/ui/dashboard/add-entities.tsx index d2765f19..e5a73410 100644 --- a/src/components/ui/dashboard/add-entities.tsx +++ b/src/components/ui/dashboard/add-entities.tsx @@ -2,21 +2,18 @@ import { useState } from "react"; import { CoachingSessionDialog } from "./coaching-session-dialog"; -import { AddCoachingSessionButton } from "./add-coaching-session-button"; import { AddMemberButton } from "./add-member-button"; import { useRouter } from "next/navigation"; import { useOrganizationStateStore } from "@/lib/providers/organization-state-store-provider"; import { useAuthStore } from "@/lib/providers/auth-store-provider"; +import CoachingSessionForm, { CoachingSessionFormMode } from "@/components/ui/dashboard/coaching-session-form"; export default function AddEntities() { const router = useRouter(); - const [open, setOpen] = useState(false); const { currentOrganizationId } = useOrganizationStateStore((state) => state); const { isCoach } = useAuthStore((state) => state); - - const onCoachingSessionAdded = () => { - setOpen(false); - }; + const mode: CoachingSessionFormMode = "create"; + const [open, setOpen] = useState(false); const onMemberButtonClicked = () => { router.push(`/organizations/${currentOrganizationId}/members`); @@ -27,28 +24,25 @@ export default function AddEntities() {

Add New

-
- + - } /> + - {/* TODO: Refactor the AddMemberButton and AddMemberDialog to work just like + {/* TODO: Refactor the AddMemberButton and AddMemberDialog to work just like AddCoachingSessionDialog does above, where the dialog is the parent container and it accepts a AddMemberButton as the dialogTrigger parameter. */} - -
+
); } diff --git a/src/components/ui/dashboard/coaching-session-dialog.tsx b/src/components/ui/dashboard/coaching-session-dialog.tsx index 1307190e..d7094800 100644 --- a/src/components/ui/dashboard/coaching-session-dialog.tsx +++ b/src/components/ui/dashboard/coaching-session-dialog.tsx @@ -1,7 +1,6 @@ "use client"; import React from "react"; -import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -10,108 +9,41 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; -import { Label } from "@/components/ui/label"; -import { Calendar } from "@/components/ui/calendar"; -import { useCoachingRelationshipStateStore } from "@/lib/providers/coaching-relationship-state-store-provider"; -import { getDateTimeFromString } from "@/types/general"; -import { - CoachingSession, - defaultCoachingSession, -} from "@/types/coaching-session"; -import { - useCoachingSessionList, - useCoachingSessionMutation, -} from "@/lib/api/coaching-sessions"; -import { DateTime } from "ts-luxon"; +import { CoachingSessionFormMode } from "./coaching-session-form"; import { useAuthStore } from "@/lib/providers/auth-store-provider"; -import { cn } from "@/components/lib/utils"; -import { format } from "date-fns"; +import { useCoachingRelationshipStateStore } from "@/lib/providers/coaching-relationship-state-store-provider"; interface CoachingSessionDialogProps { open: boolean; onOpenChange: (open: boolean) => void; - onCoachingSessionUpdated: () => void; - dialogTrigger?: React.ReactElement>; - existingSession?: CoachingSession; - mode: "create" | "update"; + mode: CoachingSessionFormMode; + children: React.ReactNode; } export function CoachingSessionDialog({ open, onOpenChange, - onCoachingSessionUpdated, - dialogTrigger, - existingSession, mode, + children, }: CoachingSessionDialogProps) { + + const { isCoach } = useAuthStore((state) => state); const { currentCoachingRelationshipId } = useCoachingRelationshipStateStore( (state) => state ); - const fromDate = DateTime.now().minus({ month: 1 }); - const toDate = DateTime.now().plus({ month: 1 }); - const { refresh } = useCoachingSessionList( - currentCoachingRelationshipId, - fromDate, - toDate - ); - const { create: createCoachingSession, update } = - useCoachingSessionMutation(); - const [sessionDate, setSessionDate] = useState( - existingSession ? new Date(existingSession.date) : undefined - ); - const [sessionTime, setSessionTime] = useState( - existingSession ? format(new Date(existingSession.date), "HH:mm") : "" - ); - const { isCoach } = useAuthStore((state) => state); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (!sessionDate || !sessionTime) return; - - const [hours, minutes] = sessionTime.split(":").map(Number); - const dateTime = getDateTimeFromString(sessionDate.toISOString()) - .set({ hour: hours, minute: minutes }) - .toFormat("yyyy-MM-dd'T'HH:mm:ss"); - - try { - if (mode === "create") { - const newCoachingSession: CoachingSession = { - ...defaultCoachingSession(), - coaching_relationship_id: currentCoachingRelationshipId, - date: dateTime, - }; - await createCoachingSession(newCoachingSession); - } else if (existingSession) { - await update(existingSession.id, { - ...existingSession, - date: dateTime, - updated_at: DateTime.now(), - }); - } - refresh(); - onCoachingSessionUpdated(); - setSessionDate(undefined); - setSessionTime(""); - onOpenChange(false); - } catch (error) { - console.error(`Failed to ${mode} coaching session:`, error); - } - }; return ( - {dialogTrigger && ( - - {React.cloneElement(dialogTrigger, { - ...(dialogTrigger.props as React.HTMLAttributes), - className: cn( - (dialogTrigger.props as React.HTMLAttributes) - .className - ), - })} - - )} - + + + @@ -120,30 +52,7 @@ export function CoachingSessionDialog({ : "Update Coaching Session"} -
-
- - setSessionDate(date)} - /> -
-
- - setSessionTime(e.target.value)} - className="w-full border rounded p-2" - required - /> -
- -
+ {children}
); diff --git a/src/components/ui/dashboard/coaching-session-form.tsx b/src/components/ui/dashboard/coaching-session-form.tsx new file mode 100644 index 00000000..63c1675c --- /dev/null +++ b/src/components/ui/dashboard/coaching-session-form.tsx @@ -0,0 +1,111 @@ + + +import { CoachingSession } from "@/types/coaching-session"; +import { Label } from "@/components/ui/label"; +import { Calendar } from "@/components/ui/calendar"; +import { Button } from "@/components/ui/button"; +import { useCoachingRelationshipStateStore } from "@/lib/providers/coaching-relationship-state-store-provider"; +import { useCoachingSessionList, useCoachingSessionMutation } from "@/lib/api/coaching-sessions"; +import { DateTime } from "ts-luxon"; +import { useAuthStore } from "@/lib/providers/auth-store-provider"; +import { format } from "date-fns"; +import { getDateTimeFromString } from "@/types/general"; +import { useState } from "react"; +import { defaultCoachingSession } from "@/types/coaching-session"; + +export type CoachingSessionFormMode = "create" | "update"; + + +interface CoachingSessionFormProps { + existingSession?: CoachingSession; + mode: CoachingSessionFormMode; + onOpenChange: (open: boolean) => void; +} + +export default function CoachingSessionForm({ + existingSession, + mode, + onOpenChange +}: CoachingSessionFormProps) { + + const { currentCoachingRelationshipId } = useCoachingRelationshipStateStore( + (state) => state + ); + const fromDate = DateTime.now().minus({ month: 1 }); + const toDate = DateTime.now().plus({ month: 1 }); + const { refresh } = useCoachingSessionList( + currentCoachingRelationshipId, + fromDate, + toDate + ); + const { create: createCoachingSession, update } = + useCoachingSessionMutation(); + const [sessionDate, setSessionDate] = useState( + existingSession ? new Date(existingSession.date) : undefined + ); + const [sessionTime, setSessionTime] = useState( + existingSession ? format(new Date(existingSession.date), "HH:mm") : "" + ); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!sessionDate || !sessionTime) return; + + const [hours, minutes] = sessionTime.split(":").map(Number); + const dateTime = getDateTimeFromString(sessionDate.toISOString()) + .set({ hour: hours, minute: minutes }) + .toFormat("yyyy-MM-dd'T'HH:mm:ss"); + + try { + if (mode === "create") { + const newCoachingSession: CoachingSession = { + ...defaultCoachingSession(), + coaching_relationship_id: currentCoachingRelationshipId, + date: dateTime, + }; + await createCoachingSession(newCoachingSession); + } else if (existingSession) { + await update(existingSession.id, { + ...existingSession, + date: dateTime, + updated_at: DateTime.now(), + }); + } + refresh(); + setSessionDate(undefined); + setSessionTime(""); + onOpenChange(false); + } catch (error) { + console.error(`Failed to ${mode} coaching session:`, error); + } + }; + + return ( +
+
+
+ + setSessionDate(date)} + /> +
+
+ + setSessionTime(e.target.value)} + className="w-full border rounded p-2" + required + /> +
+ +
+
+ ); +} \ No newline at end of file diff --git a/src/components/ui/dashboard/coaching-session-list.tsx b/src/components/ui/dashboard/coaching-session-list.tsx index d8336ced..ab17845d 100644 --- a/src/components/ui/dashboard/coaching-session-list.tsx +++ b/src/components/ui/dashboard/coaching-session-list.tsx @@ -4,11 +4,12 @@ import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { ArrowUpDown } from "lucide-react"; -import { useAuthStore } from "@/lib/providers/auth-store-provider"; + import { useCoachingRelationshipStateStore } from "@/lib/providers/coaching-relationship-state-store-provider"; import { useCoachingSessionList } from "@/lib/api/coaching-sessions"; import { CoachingSession as CoachingSessionComponent } from "@/components/ui/coaching-session"; import { DateTime } from "ts-luxon"; +import CoachingSessionForm, { CoachingSessionFormMode } from "@/components/ui/dashboard/coaching-session-form"; import { CoachingSessionDialog } from "./coaching-session-dialog"; export default function CoachingSessionList() { @@ -24,24 +25,19 @@ export default function CoachingSessionList() { coachingSessions, isLoading: isLoadingCoachingSessions, isError: isErrorCoachingSessions, - refresh, } = useCoachingSessionList(currentCoachingRelationshipId, fromDate, toDate); const [sortByDate, setSortByDate] = useState(true); const [open, setOpen] = useState(false); - const { isCoach } = useAuthStore((state) => state); + const sortedSessions = coachingSessions ? [...coachingSessions].sort((a, b) => { - return new Date(b.date).getTime() - new Date(a.date).getTime(); - }) + return new Date(b.date).getTime() - new Date(a.date).getTime(); + }) : []; - const onCoachingSessionAdded = () => { - // SWR refresh - refresh(); - setOpen(false); - }; + const mode: CoachingSessionFormMode = "create"; if (isLoadingCoachingSessions) return
Loading coaching sessions...
; if (isErrorCoachingSessions) @@ -55,21 +51,15 @@ export default function CoachingSessionList() { Coaching Sessions - Create New Coaching Session - - } - /> + mode={mode} + > + +
- - - - - - - - setCurrentCoachingSessionId(coachingSession.id) - } - > - Join Session - - - setUpdateDialogOpen(true)}> - Update Session - - - Delete Session - - - + + + + + + + setUpdateDialogOpen(true)}> + Update Session + + + Delete Session + + + +
From 704b93356a9136be6990e34b4089c4326da0f46d Mon Sep 17 00:00:00 2001 From: Caleb Bourg Date: Sat, 5 Apr 2025 07:18:06 -0400 Subject: [PATCH 05/19] fix regressions from updating coaching session dialog --- src/components/ui/coaching-session.tsx | 14 +----- src/components/ui/dashboard/add-entities.tsx | 43 +++++++++++-------- .../ui/dashboard/coaching-session-dialog.tsx | 20 +++++---- 3 files changed, 39 insertions(+), 38 deletions(-) diff --git a/src/components/ui/coaching-session.tsx b/src/components/ui/coaching-session.tsx index 4ef08cc0..205129f3 100644 --- a/src/components/ui/coaching-session.tsx +++ b/src/components/ui/coaching-session.tsx @@ -15,8 +15,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { MoreHorizontal } from "lucide-react"; -import { CoachingSessionDialog } from "@/components/ui/dashboard/coaching-session-dialog"; -import CoachingSessionForm, { CoachingSessionFormMode } from "@/components/ui/dashboard/coaching-session-form"; +import { CoachingSessionFormMode } from "@/components/ui/dashboard/coaching-session-form"; import { CoachingSession as CoachingSessionType } from "@/types/coaching-session"; interface CoachingSessionProps { @@ -72,17 +71,6 @@ const CoachingSession: React.FC = ({ - - - ); }; diff --git a/src/components/ui/dashboard/add-entities.tsx b/src/components/ui/dashboard/add-entities.tsx index e5a73410..9fca446d 100644 --- a/src/components/ui/dashboard/add-entities.tsx +++ b/src/components/ui/dashboard/add-entities.tsx @@ -2,47 +2,56 @@ import { useState } from "react"; import { CoachingSessionDialog } from "./coaching-session-dialog"; +import CoachingSessionForm, { CoachingSessionFormMode } from "./coaching-session-form"; +import { AddCoachingSessionButton } from "./add-coaching-session-button"; import { AddMemberButton } from "./add-member-button"; import { useRouter } from "next/navigation"; import { useOrganizationStateStore } from "@/lib/providers/organization-state-store-provider"; import { useAuthStore } from "@/lib/providers/auth-store-provider"; -import CoachingSessionForm, { CoachingSessionFormMode } from "@/components/ui/dashboard/coaching-session-form"; export default function AddEntities() { const router = useRouter(); const { currentOrganizationId } = useOrganizationStateStore((state) => state); const { isCoach } = useAuthStore((state) => state); - const mode: CoachingSessionFormMode = "create"; const [open, setOpen] = useState(false); const onMemberButtonClicked = () => { router.push(`/organizations/${currentOrganizationId}/members`); }; + const mode: CoachingSessionFormMode = "create"; + return (

Add New

- - + - + mode={mode} + dialogTrigger={ + + } + > + + - {/* TODO: Refactor the AddMemberButton and AddMemberDialog to work just like + {/* TODO: Refactor the AddMemberButton and AddMemberDialog to work just like AddCoachingSessionDialog does above, where the dialog is the parent container and it accepts a AddMemberButton as the dialogTrigger parameter. */} - + +
); -} +} \ No newline at end of file diff --git a/src/components/ui/dashboard/coaching-session-dialog.tsx b/src/components/ui/dashboard/coaching-session-dialog.tsx index d7094800..b730e0bb 100644 --- a/src/components/ui/dashboard/coaching-session-dialog.tsx +++ b/src/components/ui/dashboard/coaching-session-dialog.tsx @@ -17,6 +17,7 @@ interface CoachingSessionDialogProps { open: boolean; onOpenChange: (open: boolean) => void; mode: CoachingSessionFormMode; + dialogTrigger?: React.ReactElement>; children: React.ReactNode; } @@ -24,6 +25,7 @@ export function CoachingSessionDialog({ open, onOpenChange, mode, + dialogTrigger, children, }: CoachingSessionDialogProps) { @@ -35,14 +37,16 @@ export function CoachingSessionDialog({ return ( - + {dialogTrigger ?? ( + + )} From e9cb3e72837644f8769924de9daccc56f0495e7a Mon Sep 17 00:00:00 2001 From: Caleb Bourg Date: Sat, 5 Apr 2025 08:06:54 -0400 Subject: [PATCH 06/19] refactor coaching session creation/updates --- src/app/dashboard/dashboard-content.tsx | 69 ++++++++++++++++ src/app/dashboard/page.tsx | 44 +---------- src/components/ui/coaching-session.tsx | 79 +++++++++---------- src/components/ui/dashboard/add-entities.tsx | 35 +++----- .../ui/dashboard/coaching-session-dialog.tsx | 43 +++------- .../ui/dashboard/coaching-session-list.tsx | 25 ++---- 6 files changed, 136 insertions(+), 159 deletions(-) create mode 100644 src/app/dashboard/dashboard-content.tsx diff --git a/src/app/dashboard/dashboard-content.tsx b/src/app/dashboard/dashboard-content.tsx new file mode 100644 index 00000000..f1282697 --- /dev/null +++ b/src/app/dashboard/dashboard-content.tsx @@ -0,0 +1,69 @@ +"use client"; + +import { useState } from "react"; +import type * as React from "react"; +import { cn } from "@/components/lib/utils"; +import SelectCoachingRelationship from "@/components/ui/dashboard/select-coaching-relationship"; +import CoachingSessionList from "@/components/ui/dashboard/coaching-session-list"; +import AddEntities from "@/components/ui/dashboard/add-entities"; +import { CoachingSessionDialog } from "@/components/ui/dashboard/coaching-session-dialog"; +import type { CoachingSession } from "@/types/coaching-session"; + +function DashboardContainer({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
*]:w-full", + className + )} + {...props} + /> + ); +} + +export function DashboardContent() { + const [dialogOpen, setDialogOpen] = useState(false); + const [sessionToEdit, setSessionToEdit] = useState(); + + const handleOpenDialog = (session?: CoachingSession) => { + setSessionToEdit(session); + setDialogOpen(true); + }; + + const handleCloseDialog = () => { + setDialogOpen(false); + setSessionToEdit(undefined); + }; + + return ( + <> +
+
+ handleOpenDialog()} /> +
+
+ + + + + + + + ); +} diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 60868e82..28018570 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,51 +1,11 @@ import type { Metadata } from "next"; -import type * as React from "react"; -import { cn } from "@/components/lib/utils"; -import SelectCoachingRelationship from "@/components/ui/dashboard/select-coaching-relationship"; -import CoachingSessionList from "@/components/ui/dashboard/coaching-session-list"; -import AddEntities from "@/components/ui/dashboard/add-entities"; +import { DashboardContent } from "./dashboard-content"; export const metadata: Metadata = { title: "Dashboard", description: "Coaching dashboard", }; -function DashboardContainer({ - className, - ...props -}: React.HTMLAttributes) { - return ( -
*]:w-full", - className - )} - {...props} - /> - ); -} - export default function DashboardPage() { - return ( - <> -
-
- -
-
- - - - - - ); + return ; } diff --git a/src/components/ui/coaching-session.tsx b/src/components/ui/coaching-session.tsx index 205129f3..802e3791 100644 --- a/src/components/ui/coaching-session.tsx +++ b/src/components/ui/coaching-session.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState } from "react"; +import React from "react"; import { format } from "date-fns"; import { Card, CardHeader } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; @@ -15,63 +15,60 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { MoreHorizontal } from "lucide-react"; -import { CoachingSessionFormMode } from "@/components/ui/dashboard/coaching-session-form"; import { CoachingSession as CoachingSessionType } from "@/types/coaching-session"; interface CoachingSessionProps { coachingSession: CoachingSessionType; + onUpdate: () => void; } const CoachingSession: React.FC = ({ coachingSession, + onUpdate, }) => { const { setCurrentCoachingSessionId } = useCoachingSessionStateStore( (state) => state ); - const [updateDialogOpen, setUpdateDialogOpen] = useState(false); - const mode: CoachingSessionFormMode = "update"; return ( - <> - - -
-
- -
- {format(new Date(coachingSession.date), "MMMM d, yyyy h:mm a")} -
+ + +
+
+ +
+ {format(new Date(coachingSession.date), "MMMM d, yyyy h:mm a")}
-
- -
+
+ + + + + + - - - - - - - setUpdateDialogOpen(true)}> - Update Session - - - Delete Session - - - -
+ + + + Update Session + + + Delete Session + + +
- - - +
+
+
); }; diff --git a/src/components/ui/dashboard/add-entities.tsx b/src/components/ui/dashboard/add-entities.tsx index 9fca446d..dfe2cf38 100644 --- a/src/components/ui/dashboard/add-entities.tsx +++ b/src/components/ui/dashboard/add-entities.tsx @@ -1,52 +1,35 @@ "use client"; -import { useState } from "react"; -import { CoachingSessionDialog } from "./coaching-session-dialog"; -import CoachingSessionForm, { CoachingSessionFormMode } from "./coaching-session-form"; import { AddCoachingSessionButton } from "./add-coaching-session-button"; import { AddMemberButton } from "./add-member-button"; import { useRouter } from "next/navigation"; import { useOrganizationStateStore } from "@/lib/providers/organization-state-store-provider"; import { useAuthStore } from "@/lib/providers/auth-store-provider"; -export default function AddEntities() { +interface AddEntitiesProps { + onCreateSession: () => void; +} + +export default function AddEntities({ onCreateSession }: AddEntitiesProps) { const router = useRouter(); const { currentOrganizationId } = useOrganizationStateStore((state) => state); const { isCoach } = useAuthStore((state) => state); - const [open, setOpen] = useState(false); const onMemberButtonClicked = () => { router.push(`/organizations/${currentOrganizationId}/members`); }; - const mode: CoachingSessionFormMode = "create"; - return (

Add New

- - } - > - - + - {/* TODO: Refactor the AddMemberButton and AddMemberDialog to work just like - AddCoachingSessionDialog does above, where the dialog is the parent container - and it accepts a AddMemberButton as the dialogTrigger parameter. - */} void; - mode: CoachingSessionFormMode; - dialogTrigger?: React.ReactElement>; - children: React.ReactNode; + coachingSessionToEdit?: CoachingSession; } export function CoachingSessionDialog({ open, onOpenChange, - mode, - dialogTrigger, - children, + coachingSessionToEdit, }: CoachingSessionDialogProps) { - - const { isCoach } = useAuthStore((state) => state); - const { currentCoachingRelationshipId } = useCoachingRelationshipStateStore( - (state) => state - ); + const mode: CoachingSessionFormMode = coachingSessionToEdit ? "update" : "create"; return ( - - {dialogTrigger ?? ( - - )} - - {mode === "create" - ? "Create New Coaching Session" - : "Update Coaching Session"} + {mode === "create" ? "Create New Coaching Session" : "Update Coaching Session"} - {children} + ); diff --git a/src/components/ui/dashboard/coaching-session-list.tsx b/src/components/ui/dashboard/coaching-session-list.tsx index ab17845d..e33f30c6 100644 --- a/src/components/ui/dashboard/coaching-session-list.tsx +++ b/src/components/ui/dashboard/coaching-session-list.tsx @@ -4,15 +4,17 @@ import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { ArrowUpDown } from "lucide-react"; - import { useCoachingRelationshipStateStore } from "@/lib/providers/coaching-relationship-state-store-provider"; import { useCoachingSessionList } from "@/lib/api/coaching-sessions"; import { CoachingSession as CoachingSessionComponent } from "@/components/ui/coaching-session"; import { DateTime } from "ts-luxon"; -import CoachingSessionForm, { CoachingSessionFormMode } from "@/components/ui/dashboard/coaching-session-form"; -import { CoachingSessionDialog } from "./coaching-session-dialog"; +import type { CoachingSession } from "@/types/coaching-session"; + +interface CoachingSessionListProps { + onUpdateSession: (session: CoachingSession) => void; +} -export default function CoachingSessionList() { +export default function CoachingSessionList({ onUpdateSession }: CoachingSessionListProps) { const { currentCoachingRelationshipId } = useCoachingRelationshipStateStore( (state) => state ); @@ -28,8 +30,6 @@ export default function CoachingSessionList() { } = useCoachingSessionList(currentCoachingRelationshipId, fromDate, toDate); const [sortByDate, setSortByDate] = useState(true); - const [open, setOpen] = useState(false); - const sortedSessions = coachingSessions ? [...coachingSessions].sort((a, b) => { @@ -37,8 +37,6 @@ export default function CoachingSessionList() { }) : []; - const mode: CoachingSessionFormMode = "create"; - if (isLoadingCoachingSessions) return
Loading coaching sessions...
; if (isErrorCoachingSessions) return
Error loading coaching sessions
; @@ -50,16 +48,6 @@ export default function CoachingSessionList() { Coaching Sessions - - -
From d79bbce1eccf29d1eb9ee23f4ed00ebbd4872094 Mon Sep 17 00:00:00 2001 From: Caleb Bourg Date: Sat, 5 Apr 2025 09:10:57 -0400 Subject: [PATCH 07/19] refresh coaching session list after delete --- src/components/ui/coaching-session.tsx | 4 +++- .../ui/dashboard/coaching-session-list.tsx | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/components/ui/coaching-session.tsx b/src/components/ui/coaching-session.tsx index 802e3791..09717a9b 100644 --- a/src/components/ui/coaching-session.tsx +++ b/src/components/ui/coaching-session.tsx @@ -20,11 +20,13 @@ import { CoachingSession as CoachingSessionType } from "@/types/coaching-session interface CoachingSessionProps { coachingSession: CoachingSessionType; onUpdate: () => void; + onDelete: () => void; } const CoachingSession: React.FC = ({ coachingSession, onUpdate, + onDelete, }) => { const { setCurrentCoachingSessionId } = useCoachingSessionStateStore( (state) => state @@ -60,7 +62,7 @@ const CoachingSession: React.FC = ({ Update Session - + Delete Session diff --git a/src/components/ui/dashboard/coaching-session-list.tsx b/src/components/ui/dashboard/coaching-session-list.tsx index e33f30c6..5aca6dec 100644 --- a/src/components/ui/dashboard/coaching-session-list.tsx +++ b/src/components/ui/dashboard/coaching-session-list.tsx @@ -6,9 +6,11 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { ArrowUpDown } from "lucide-react"; import { useCoachingRelationshipStateStore } from "@/lib/providers/coaching-relationship-state-store-provider"; import { useCoachingSessionList } from "@/lib/api/coaching-sessions"; +import { useCoachingSessionMutation } from "@/lib/api/coaching-sessions"; import { CoachingSession as CoachingSessionComponent } from "@/components/ui/coaching-session"; import { DateTime } from "ts-luxon"; import type { CoachingSession } from "@/types/coaching-session"; +import { Id } from "@/types/general"; interface CoachingSessionListProps { onUpdateSession: (session: CoachingSession) => void; @@ -27,8 +29,24 @@ export default function CoachingSessionList({ onUpdateSession }: CoachingSession coachingSessions, isLoading: isLoadingCoachingSessions, isError: isErrorCoachingSessions, + refresh: refreshCoachingSessions, } = useCoachingSessionList(currentCoachingRelationshipId, fromDate, toDate); + const { delete: deleteCoachingSession } = useCoachingSessionMutation(); + + const handleDeleteCoachingSession = async (id: Id) => { + if (!confirm("Are you sure you want to delete this session?")) { + return; + } + + try { + await deleteCoachingSession(id).then(() => refreshCoachingSessions()); + } catch (error) { + console.error("Error deleting coaching session:", error); + // TODO: Show an error toast here once we start using toasts for showing operation results. + } + }; + const [sortByDate, setSortByDate] = useState(true); const sortedSessions = coachingSessions @@ -84,6 +102,7 @@ export default function CoachingSessionList({ onUpdateSession }: CoachingSession key={coachingSession.id} coachingSession={coachingSession} onUpdate={() => onUpdateSession(coachingSession)} + onDelete={() => handleDeleteCoachingSession(coachingSession.id)} /> ))}
From dbc696678c8a8e6877934d0e382d18b4b9af6ab4 Mon Sep 17 00:00:00 2001 From: Caleb Bourg Date: Sat, 5 Apr 2025 09:20:34 -0400 Subject: [PATCH 08/19] only coaches can delete coaching sessions --- src/components/ui/coaching-session.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/ui/coaching-session.tsx b/src/components/ui/coaching-session.tsx index 09717a9b..2b59b756 100644 --- a/src/components/ui/coaching-session.tsx +++ b/src/components/ui/coaching-session.tsx @@ -16,6 +16,7 @@ import { } from "@/components/ui/dropdown-menu"; import { MoreHorizontal } from "lucide-react"; import { CoachingSession as CoachingSessionType } from "@/types/coaching-session"; +import { useAuthStore } from "@/lib/providers/auth-store-provider"; interface CoachingSessionProps { coachingSession: CoachingSessionType; @@ -31,6 +32,7 @@ const CoachingSession: React.FC = ({ const { setCurrentCoachingSessionId } = useCoachingSessionStateStore( (state) => state ); + const { isCoach } = useAuthStore((state) => state); return ( @@ -62,9 +64,11 @@ const CoachingSession: React.FC = ({ Update Session - - Delete Session - + {isCoach && ( + + Delete Session + + )}
From 962040be2ac9effb2c23297430cc0e3eaba182da Mon Sep 17 00:00:00 2001 From: Jim Hodapp Date: Sat, 5 Apr 2025 16:33:08 -0500 Subject: [PATCH 09/19] Removes the SelectCoachingRelationship component from the Dashboard page. Moves CoachingSessionList left. Adds a tab bar splitting sessions out by upcoming/previous sessions. --- src/app/dashboard/dashboard-content.tsx | 8 +- .../ui/dashboard/coaching-session-list.tsx | 173 +++++++++++++----- src/types/coaching-session.ts | 51 +++++- 3 files changed, 177 insertions(+), 55 deletions(-) diff --git a/src/app/dashboard/dashboard-content.tsx b/src/app/dashboard/dashboard-content.tsx index f1282697..96f0dce7 100644 --- a/src/app/dashboard/dashboard-content.tsx +++ b/src/app/dashboard/dashboard-content.tsx @@ -35,7 +35,9 @@ function DashboardContainer({ export function DashboardContent() { const [dialogOpen, setDialogOpen] = useState(false); - const [sessionToEdit, setSessionToEdit] = useState(); + const [sessionToEdit, setSessionToEdit] = useState< + CoachingSession | undefined + >(); const handleOpenDialog = (session?: CoachingSession) => { setSessionToEdit(session); @@ -54,8 +56,8 @@ export function DashboardContent() { handleOpenDialog()} />
- - + + {/* */} diff --git a/src/components/ui/dashboard/coaching-session-list.tsx b/src/components/ui/dashboard/coaching-session-list.tsx index 5aca6dec..ab83f97c 100644 --- a/src/components/ui/dashboard/coaching-session-list.tsx +++ b/src/components/ui/dashboard/coaching-session-list.tsx @@ -9,14 +9,20 @@ import { useCoachingSessionList } from "@/lib/api/coaching-sessions"; import { useCoachingSessionMutation } from "@/lib/api/coaching-sessions"; import { CoachingSession as CoachingSessionComponent } from "@/components/ui/coaching-session"; import { DateTime } from "ts-luxon"; -import type { CoachingSession } from "@/types/coaching-session"; -import { Id } from "@/types/general"; +import { + filterAndSortCoachingSessions, + type CoachingSession, +} from "@/types/coaching-session"; +import { Id, SortOrder } from "@/types/general"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; interface CoachingSessionListProps { onUpdateSession: (session: CoachingSession) => void; } -export default function CoachingSessionList({ onUpdateSession }: CoachingSessionListProps) { +export default function CoachingSessionList({ + onUpdateSession, +}: CoachingSessionListProps) { const { currentCoachingRelationshipId } = useCoachingRelationshipStateStore( (state) => state ); @@ -47,12 +53,18 @@ export default function CoachingSessionList({ onUpdateSession }: CoachingSession } }; - const [sortByDate, setSortByDate] = useState(true); + // const [sortByDate, setSortByDate] = useState(true); - const sortedSessions = coachingSessions - ? [...coachingSessions].sort((a, b) => { - return new Date(b.date).getTime() - new Date(a.date).getTime(); - }) + const upcomingSessions = coachingSessions + ? filterAndSortCoachingSessions(coachingSessions, SortOrder.Ascending, true) + : []; + + const previousSessions = coachingSessions + ? filterAndSortCoachingSessions( + coachingSessions, + SortOrder.Descending, + false + ) : []; if (isLoadingCoachingSessions) return
Loading coaching sessions...
; @@ -60,33 +72,9 @@ export default function CoachingSessionList({ onUpdateSession }: CoachingSession return
Error loading coaching sessions
; return ( - - -
- - Coaching Sessions - -
-
- - -
+ + + Coaching Sessions {!currentCoachingRelationshipId ? ( @@ -96,18 +84,113 @@ export default function CoachingSessionList({ onUpdateSession }: CoachingSession

) : ( -
- {sortedSessions.map((coachingSession) => ( - onUpdateSession(coachingSession)} - onDelete={() => handleDeleteCoachingSession(coachingSession.id)} - /> - ))} -
+ + + Upcoming + Previous + + + {/*
+ + +
*/} +
+ {upcomingSessions.map((coachingSession) => ( + onUpdateSession(coachingSession)} + onDelete={() => + handleDeleteCoachingSession(coachingSession.id) + } + /> + ))} +
+
+ +
+ {previousSessions.map((coachingSession) => ( + onUpdateSession(coachingSession)} + onDelete={() => + handleDeleteCoachingSession(coachingSession.id) + } + /> + ))} +
+
+
)} ); + // return ( + // + // + //
+ // + // Coaching Sessions + // + //
+ //
+ // + // + //
+ //
+ // + // {!currentCoachingRelationshipId ? ( + //
+ //

+ // Choose a Relationship to view Coaching Sessions + //

+ //
+ // ) : ( + //
+ // {sortedSessions.map((coachingSession) => ( + // onUpdateSession(coachingSession)} + // onDelete={() => handleDeleteCoachingSession(coachingSession.id)} + // /> + // ))} + //
+ // )} + //
+ //
+ // ); } diff --git a/src/types/coaching-session.ts b/src/types/coaching-session.ts index 348f09ae..e316a257 100644 --- a/src/types/coaching-session.ts +++ b/src/types/coaching-session.ts @@ -32,26 +32,63 @@ export function isCoachingSessionArray( return Array.isArray(value) && value.every(isCoachingSession); } -export function sortCoachingSessionArray( +export function filterAndSortCoachingSessions( sessions: CoachingSession[], - order: SortOrder + order: SortOrder, + returnUpcoming: boolean ): CoachingSession[] { - if (order == SortOrder.Ascending) { - sessions.sort( + const now = new Date(); + + // Filter sessions based on the `showUpcoming` parameter + const filteredSessions = sessions.filter((session) => { + const sessionDate = new Date(session.date.toString()); + if (returnUpcoming) { + // Include sessions today that haven't started yet or are in the future + return sessionDate >= now; + } else { + // Include past sessions only + return sessionDate < now; + } + }); + + // Sort the filtered sessions based on the order parameter + if (order === SortOrder.Ascending) { + filteredSessions.sort( (a, b) => new Date(a.date.toString()).getTime() - new Date(b.date.toString()).getTime() ); - } else if (order == SortOrder.Descending) { - sessions.sort( + } else if (order === SortOrder.Descending) { + filteredSessions.sort( (a, b) => new Date(b.date.toString()).getTime() - new Date(a.date.toString()).getTime() ); } - return sessions; + + return filteredSessions; } +// export function sortCoachingSessionArray( +// sessions: CoachingSession[], +// order: SortOrder +// ): CoachingSession[] { +// if (order == SortOrder.Ascending) { +// sessions.sort( +// (a, b) => +// new Date(a.date.toString()).getTime() - +// new Date(b.date.toString()).getTime() +// ); +// } else if (order == SortOrder.Descending) { +// sessions.sort( +// (a, b) => +// new Date(b.date.toString()).getTime() - +// new Date(a.date.toString()).getTime() +// ); +// } +// return sessions; +// } + export function getCoachingSessionById( id: string, sessions: CoachingSession[] From 39821099a3590c128d239ebd26526251648b0969 Mon Sep 17 00:00:00 2001 From: Jim Hodapp Date: Mon, 7 Apr 2025 16:38:08 -0500 Subject: [PATCH 10/19] Move the CoachingSessionSelector into the CoachingSessionList, refactor the layout of this component on the Dashboard page, and make logout more robust. --- src/app/dashboard/dashboard-content.tsx | 14 +- .../ui/coaching-relationship-selector.tsx | 49 +-- src/components/ui/dashboard/add-entities.tsx | 11 +- .../ui/dashboard/coaching-session-list.tsx | 139 +++------ src/components/ui/main-nav-menu.tsx | 278 ------------------ src/components/ui/user-nav.tsx | 20 +- src/lib/api/users.ts | 2 +- src/middleware.ts | 6 +- 8 files changed, 100 insertions(+), 419 deletions(-) delete mode 100644 src/components/ui/main-nav-menu.tsx diff --git a/src/app/dashboard/dashboard-content.tsx b/src/app/dashboard/dashboard-content.tsx index 96f0dce7..c1a5da09 100644 --- a/src/app/dashboard/dashboard-content.tsx +++ b/src/app/dashboard/dashboard-content.tsx @@ -20,8 +20,6 @@ function DashboardContainer({ "p-4", // Mobile: stack vertically "flex flex-col gap-6", - // Tablet and up (640px+): side by side - "sm:grid sm:grid-cols-2", // Never grow wider than the site-header "max-w-screen-2xl", // Ensure full width for children @@ -51,13 +49,11 @@ export function DashboardContent() { return ( <> -
-
- handleOpenDialog()} /> -
-
- - {/* */} + + handleOpenDialog()} + /> diff --git a/src/components/ui/coaching-relationship-selector.tsx b/src/components/ui/coaching-relationship-selector.tsx index cb75ddab..09fb8ad0 100644 --- a/src/components/ui/coaching-relationship-selector.tsx +++ b/src/components/ui/coaching-relationship-selector.tsx @@ -16,6 +16,7 @@ import { useCoachingRelationshipStateStore } from "@/lib/providers/coaching-rela import { useCoachingSessionStateStore } from "@/lib/providers/coaching-session-state-store-provider"; interface CoachingRelationshipsSelectorProps extends PopoverProps { + className?: string; /// The Organization's Id for which to get a list of associated CoachingRelationships organizationId: Id; /// Disable the component from interaction with the user @@ -63,6 +64,7 @@ function CoachingRelationshipsSelectItems({ } export default function CoachingRelationshipSelector({ + className, organizationId, disabled, onSelect, @@ -101,29 +103,32 @@ export default function CoachingRelationshipSelector({ ? getCurrentCoachingRelationship(currentCoachingRelationshipId) : null; - const displayValue = currentRelationship ? ( - <> - {currentRelationship.coach_first_name}{" "} - {currentRelationship.coach_last_name} ->{" "} - {currentRelationship.coachee_first_name}{" "} - {currentRelationship.coachee_last_name} - - ) : undefined; + const displayValue = + currentRelationship && currentRelationship.id ? ( + <> + {currentRelationship.coach_first_name}{" "} + {currentRelationship.coach_last_name} ->{" "} + {currentRelationship.coachee_first_name}{" "} + {currentRelationship.coachee_last_name} + + ) : undefined; return ( - +
+ +
); } diff --git a/src/components/ui/dashboard/add-entities.tsx b/src/components/ui/dashboard/add-entities.tsx index dfe2cf38..38ccc504 100644 --- a/src/components/ui/dashboard/add-entities.tsx +++ b/src/components/ui/dashboard/add-entities.tsx @@ -5,12 +5,17 @@ import { AddMemberButton } from "./add-member-button"; import { useRouter } from "next/navigation"; import { useOrganizationStateStore } from "@/lib/providers/organization-state-store-provider"; import { useAuthStore } from "@/lib/providers/auth-store-provider"; +import { cn } from "@/components/lib/utils"; interface AddEntitiesProps { + className?: string; onCreateSession: () => void; } -export default function AddEntities({ onCreateSession }: AddEntitiesProps) { +export default function AddEntities({ + className, + onCreateSession, +}: AddEntitiesProps) { const router = useRouter(); const { currentOrganizationId } = useOrganizationStateStore((state) => state); const { isCoach } = useAuthStore((state) => state); @@ -20,7 +25,7 @@ export default function AddEntities({ onCreateSession }: AddEntitiesProps) { }; return ( -
+

Add New

@@ -37,4 +42,4 @@ export default function AddEntities({ onCreateSession }: AddEntitiesProps) {
); -} \ No newline at end of file +} diff --git a/src/components/ui/dashboard/coaching-session-list.tsx b/src/components/ui/dashboard/coaching-session-list.tsx index ab83f97c..71d4c330 100644 --- a/src/components/ui/dashboard/coaching-session-list.tsx +++ b/src/components/ui/dashboard/coaching-session-list.tsx @@ -1,9 +1,6 @@ "use client"; -import { useState } from "react"; -import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { ArrowUpDown } from "lucide-react"; import { useCoachingRelationshipStateStore } from "@/lib/providers/coaching-relationship-state-store-provider"; import { useCoachingSessionList } from "@/lib/api/coaching-sessions"; import { useCoachingSessionMutation } from "@/lib/api/coaching-sessions"; @@ -15,14 +12,20 @@ import { } from "@/types/coaching-session"; import { Id, SortOrder } from "@/types/general"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import CoachingRelationshipSelector from "../coaching-relationship-selector"; +import { useOrganizationStateStore } from "@/lib/providers/organization-state-store-provider"; +import { cn } from "@/components/lib/utils"; interface CoachingSessionListProps { + className?: string; onUpdateSession: (session: CoachingSession) => void; } export default function CoachingSessionList({ + className, onUpdateSession, }: CoachingSessionListProps) { + const { currentOrganizationId } = useOrganizationStateStore((state) => state); const { currentCoachingRelationshipId } = useCoachingRelationshipStateStore( (state) => state ); @@ -53,8 +56,6 @@ export default function CoachingSessionList({ } }; - // const [sortByDate, setSortByDate] = useState(true); - const upcomingSessions = coachingSessions ? filterAndSortCoachingSessions(coachingSessions, SortOrder.Ascending, true) : []; @@ -67,49 +68,42 @@ export default function CoachingSessionList({ ) : []; + let noCoachingSessions = ( +
+

+ Select a coaching relationship to view Coaching Sessions +

+
+ ); + if (isLoadingCoachingSessions) return
Loading coaching sessions...
; if (isErrorCoachingSessions) return
Error loading coaching sessions
; return ( - + - Coaching Sessions + +
+
Coaching Sessions
+ +
+
- {!currentCoachingRelationshipId ? ( -
-

- Choose a Relationship to view Coaching Sessions -

-
- ) : ( - - - Upcoming - Previous - - - {/*
- - -
*/} + + + Upcoming + Previous + + + {!currentCoachingRelationshipId ? ( + noCoachingSessions + ) : (
{upcomingSessions.map((coachingSession) => ( ))}
-
- + )} + + + {!currentCoachingRelationshipId ? ( + noCoachingSessions + ) : (
{previousSessions.map((coachingSession) => ( ))}
-
-
- )} + )} +
+
); - // return ( - // - // - //
- // - // Coaching Sessions - // - //
- //
- // - // - //
- //
- // - // {!currentCoachingRelationshipId ? ( - //
- //

- // Choose a Relationship to view Coaching Sessions - //

- //
- // ) : ( - //
- // {sortedSessions.map((coachingSession) => ( - // onUpdateSession(coachingSession)} - // onDelete={() => handleDeleteCoachingSession(coachingSession.id)} - // /> - // ))} - //
- // )} - //
- //
- // ); } diff --git a/src/components/ui/main-nav-menu.tsx b/src/components/ui/main-nav-menu.tsx deleted file mode 100644 index 3c8fecb9..00000000 --- a/src/components/ui/main-nav-menu.tsx +++ /dev/null @@ -1,278 +0,0 @@ -"use client"; - -import { cn } from "@/components/lib/utils"; - -import { Dialog } from "@radix-ui/react-dialog"; - -import { Button } from "@/components/ui/button"; -import { - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Icons } from "@/components/ui/icons"; -import { - NavigationMenu, - NavigationMenuContent, - NavigationMenuItem, - NavigationMenuLink, - NavigationMenuList, - NavigationMenuTrigger, -} from "@/components/ui/navigation-menu"; -import { PresetSelector } from "@/components/ui/preset-selector"; - -import { current, future, past } from "../../data/presets"; - -import { ChevronDownIcon } from "@radix-ui/react-icons"; - -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from "@/components/ui/command"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { siteConfig } from "@/site.config"; -import { forwardRef, useState } from "react"; -import { useOrganizationStateStore } from "@/lib/providers/organization-state-store-provider"; - -export function MainNavMenu() { - const [open, setIsOpen] = useState(false); - const { currentOrganizationId, getCurrentOrganization } = - useOrganizationStateStore((state) => state); - - const organization = getCurrentOrganization(currentOrganizationId); - - return ( - <> - - - - - - {organization.name.length > 0 - ? organization.name - : siteConfig.name} - - - -
    -
  • - - - -
    - {organization.name.length > 0 - ? organization.name - : siteConfig.name} -
    -

    - {siteConfig.description} -

    -
    -
    -
  • - - {/* Hidden for MVP */} - - - Organization & Relationship - - Set your current organization and coaching relationship. - - - -
    -
    -
    -

    - Organization -

    -
    -
    - - - - - - - - - No roles found. - - -

    Viewer

    -

    - Can view and comment. -

    -
    - -

    Developer

    -

    - Can view, comment and edit. -

    -
    - -

    Billing

    -

    - Can view, comment and manage billing. -

    -
    - -

    Owner

    -

    - Admin-level access to all resources. -

    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -

    - Relationship -

    -
    -
    - - - - - - - - - No roles found. - - -

    Viewer

    -

    - Can view and comment. -

    -
    - -

    Developer

    -

    - Can view, comment and edit. -

    -
    - -

    Billing

    -

    - Can view, comment and manage billing. -

    -
    - -

    Owner

    -

    - Admin-level access to all resources. -

    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
-
-
-
-
- - - - - Current Organization & Coaching Relationship - - - Select your current organization and coaching relationship to - access specific coaching sessions across multiple organizations - and coaches/coachees. - - -
-

- Select Your Organization -

-
- -
-

- Select Your Coaching Relationship -

-
- -
-
- -
- - -
-
-
-
- - ); -} - -const ListItem = forwardRef< - React.ElementRef<"a">, - React.ComponentPropsWithoutRef<"a"> ->(({ className, title, children, ...props }, ref) => { - return ( -
  • - - -
    {title}
    -

    - {children} -

    -
    -
    -
  • - ); -}); -ListItem.displayName = "ListItem"; diff --git a/src/components/ui/user-nav.tsx b/src/components/ui/user-nav.tsx index 0d3ad7fc..275fd188 100644 --- a/src/components/ui/user-nav.tsx +++ b/src/components/ui/user-nav.tsx @@ -40,9 +40,6 @@ export function UserNav() { async function logout_user() { try { - console.trace("Deleting active user session: ", userSession.id); - await deleteUserSession(userSession.id); - console.trace("Resetting CoachingSessionStateStore state"); resetCoachingSessionState(); @@ -52,12 +49,21 @@ export function UserNav() { console.trace("Resetting OrganizationStateStore state"); resetOrganizationState(); + console.trace( + "Deleting current user session from backend: ", + userSession.id + ); + await deleteUserSession(userSession.id); + } catch (err) { + console.warn("Error while logging out session: ", userSession.id, err); + } finally { + // Ensure we still log out of the frontend even if the backend request + // to delete the user session fails. console.trace("Resetting AuthStore state"); logout(); - - router.push("/"); - } catch (err) { - console.error("Error while logging out session: ", userSession.id, err); + console.debug("Navigating to /"); + await router.push("/"); + console.debug("Navigation to / completed successfully."); } } diff --git a/src/lib/api/users.ts b/src/lib/api/users.ts index 7385ba2e..7498266f 100644 --- a/src/lib/api/users.ts +++ b/src/lib/api/users.ts @@ -55,7 +55,7 @@ export const UserApi = { * @param userId The ID of the user to delete * @returns Promise resolving to the deleted User object */ - deleteNested: async (_entityId: Id, userId: Id): Promise => { + deleteNested: async (_entityId: Id, _userId: Id): Promise => { throw new Error("Delete nested operation not implemented"); }, }; diff --git a/src/middleware.ts b/src/middleware.ts index 983f80b7..f35ede54 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -19,18 +19,18 @@ export default async function middleware(req: NextRequest) { // 3. Get the session from the cookie const sessionCookie = req.cookies.get("id"); - const session = sessionCookie?.value; + const validSession = sessionCookie?.value; // 4. Redirect to / if the user is not authenticated // 4b. TODO: Check session validity/expiration? - if (isProtectedRoute && !session) { + if (isProtectedRoute && !validSession) { return NextResponse.redirect(new URL("/", req.nextUrl)); } // 5. Redirect to /dashboard if the user is authenticated if ( isPublicRoute && - session && + validSession && !req.nextUrl.pathname.startsWith("/dashboard") ) { return NextResponse.redirect(new URL("/dashboard", req.nextUrl)); From c851b3a279318fc170e2363693694fc1283baa39 Mon Sep 17 00:00:00 2001 From: Jim Hodapp Date: Mon, 7 Apr 2025 16:44:39 -0500 Subject: [PATCH 11/19] Clean up the select coaching relationship placeholder text to be clearer and nicer to read. --- src/components/ui/dashboard/coaching-session-list.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ui/dashboard/coaching-session-list.tsx b/src/components/ui/dashboard/coaching-session-list.tsx index 71d4c330..d058ec57 100644 --- a/src/components/ui/dashboard/coaching-session-list.tsx +++ b/src/components/ui/dashboard/coaching-session-list.tsx @@ -71,7 +71,7 @@ export default function CoachingSessionList({ let noCoachingSessions = (

    - Select a coaching relationship to view Coaching Sessions + Select a coaching relationship to view your coaching sessions.

    ); From 28cd840e7d4b83d035c37c1a50ac9863abbc5c36 Mon Sep 17 00:00:00 2001 From: Jim Hodapp Date: Mon, 7 Apr 2025 17:13:27 -0500 Subject: [PATCH 12/19] We no longer need to debug print relationships. --- src/components/ui/coaching-relationship-selector.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/ui/coaching-relationship-selector.tsx b/src/components/ui/coaching-relationship-selector.tsx index 09fb8ad0..85c780a9 100644 --- a/src/components/ui/coaching-relationship-selector.tsx +++ b/src/components/ui/coaching-relationship-selector.tsx @@ -49,8 +49,6 @@ function CoachingRelationshipsSelectItems({ if (isError) return
    Error loading coaching relationships
    ; if (!relationships?.length) return
    No coaching relationships found
    ; - console.debug(`relationships: ${JSON.stringify(relationships)}`); - return ( <> {relationships.map((rel) => ( From e29da278817688d65e379259ab8b4cf6b43037da Mon Sep 17 00:00:00 2001 From: Jim Hodapp Date: Mon, 7 Apr 2025 17:14:11 -0500 Subject: [PATCH 13/19] We no longer need to debug print relationships in useEffect either. --- src/components/ui/coaching-relationship-selector.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/ui/coaching-relationship-selector.tsx b/src/components/ui/coaching-relationship-selector.tsx index 85c780a9..f425ac68 100644 --- a/src/components/ui/coaching-relationship-selector.tsx +++ b/src/components/ui/coaching-relationship-selector.tsx @@ -39,9 +39,6 @@ function CoachingRelationshipsSelectItems({ // Be sure to cache the list of current coaching relationships in the CoachingRelationshipStateStore useEffect(() => { if (!relationships.length) return; - console.debug( - `relationships (useEffect): ${JSON.stringify(relationships)}` - ); setCurrentCoachingRelationships(relationships); }, [relationships, setCurrentCoachingRelationships]); From 6e64e9954bc0a933339dfc3dc39432564590df70 Mon Sep 17 00:00:00 2001 From: Jim Hodapp Date: Mon, 7 Apr 2025 17:43:33 -0500 Subject: [PATCH 14/19] Handle loading/loading error cases better for the coaching session list. --- src/app/dashboard/dashboard-content.tsx | 1 - .../ui/coaching-relationship-selector.tsx | 3 +- .../ui/dashboard/coaching-session-list.tsx | 30 +++++++++++++++---- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/app/dashboard/dashboard-content.tsx b/src/app/dashboard/dashboard-content.tsx index c1a5da09..3386ff92 100644 --- a/src/app/dashboard/dashboard-content.tsx +++ b/src/app/dashboard/dashboard-content.tsx @@ -3,7 +3,6 @@ import { useState } from "react"; import type * as React from "react"; import { cn } from "@/components/lib/utils"; -import SelectCoachingRelationship from "@/components/ui/dashboard/select-coaching-relationship"; import CoachingSessionList from "@/components/ui/dashboard/coaching-session-list"; import AddEntities from "@/components/ui/dashboard/add-entities"; import { CoachingSessionDialog } from "@/components/ui/dashboard/coaching-session-dialog"; diff --git a/src/components/ui/coaching-relationship-selector.tsx b/src/components/ui/coaching-relationship-selector.tsx index f425ac68..1ca7f24f 100644 --- a/src/components/ui/coaching-relationship-selector.tsx +++ b/src/components/ui/coaching-relationship-selector.tsx @@ -14,6 +14,7 @@ import { useEffect } from "react"; import { useAuthStore } from "@/lib/providers/auth-store-provider"; import { useCoachingRelationshipStateStore } from "@/lib/providers/coaching-relationship-state-store-provider"; import { useCoachingSessionStateStore } from "@/lib/providers/coaching-session-state-store-provider"; +import { cn } from "../lib/utils"; interface CoachingRelationshipsSelectorProps extends PopoverProps { className?: string; @@ -109,7 +110,7 @@ export default function CoachingRelationshipSelector({ ) : undefined; return ( -
    +