diff --git a/src/app/dashboard/dashboard-content.tsx b/src/app/dashboard/dashboard-content.tsx index f1282697..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"; @@ -20,8 +19,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 @@ -35,7 +32,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); @@ -49,13 +48,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..1ca7f24f 100644 --- a/src/components/ui/coaching-relationship-selector.tsx +++ b/src/components/ui/coaching-relationship-selector.tsx @@ -14,8 +14,10 @@ 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; /// The Organization's Id for which to get a list of associated CoachingRelationships organizationId: Id; /// Disable the component from interaction with the user @@ -38,9 +40,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]); @@ -48,8 +47,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) => ( @@ -63,6 +60,7 @@ function CoachingRelationshipsSelectItems({ } export default function CoachingRelationshipSelector({ + className, organizationId, disabled, onSelect, @@ -101,29 +99,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/coaching-session.tsx b/src/components/ui/coaching-session.tsx index 2b59b756..0b381531 100644 --- a/src/components/ui/coaching-session.tsx +++ b/src/components/ui/coaching-session.tsx @@ -62,11 +62,11 @@ const CoachingSession: React.FC = ({ - Update Session + Edit {isCoach && ( - Delete Session + Delete )} 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 5aca6dec..31c697d8 100644 --- a/src/components/ui/dashboard/coaching-session-list.tsx +++ b/src/components/ui/dashboard/coaching-session-list.tsx @@ -1,22 +1,31 @@ "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"; 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"; +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({ onUpdateSession }: CoachingSessionListProps) { +export default function CoachingSessionList({ + className, + onUpdateSession, +}: CoachingSessionListProps) { + const { currentOrganizationId } = useOrganizationStateStore((state) => state); const { currentCoachingRelationshipId } = useCoachingRelationshipStateStore( (state) => state ); @@ -47,66 +56,107 @@ export default function CoachingSessionList({ onUpdateSession }: CoachingSession } }; - const [sortByDate, setSortByDate] = useState(true); + const upcomingSessions = coachingSessions + ? filterAndSortCoachingSessions(coachingSessions, SortOrder.Ascending, true) + : []; - const sortedSessions = coachingSessions - ? [...coachingSessions].sort((a, b) => { - return new Date(b.date).getTime() - new Date(a.date).getTime(); - }) + const previousSessions = coachingSessions + ? filterAndSortCoachingSessions( + coachingSessions, + SortOrder.Descending, + false + ) : []; - if (isLoadingCoachingSessions) return
Loading coaching sessions...
; - if (isErrorCoachingSessions) - return
Error loading coaching sessions
; + let loadingCoachingSessions = ( +
+

+ Loading your coaching sessions... +

+
+ ); + + let noCoachingSessions = ( +
+

+ Select a coaching relationship to view your coaching sessions. +

+
+ ); + + let errorLoadingCoachingSessions = ( +
+

+ There was an error trying to load your coaching sessions. +

+
+ ); return ( - - -
- - Coaching Sessions - -
-
- - -
+ + + +
+
Coaching Sessions
+ +
+
- {!currentCoachingRelationshipId ? ( -
-

- Choose a Relationship to view Coaching Sessions -

-
- ) : ( -
- {sortedSessions.map((coachingSession) => ( - onUpdateSession(coachingSession)} - onDelete={() => handleDeleteCoachingSession(coachingSession.id)} - /> - ))} -
- )} + + + Upcoming + Previous + + + {isLoadingCoachingSessions ? ( + loadingCoachingSessions + ) : isErrorCoachingSessions ? ( + errorLoadingCoachingSessions + ) : !currentCoachingRelationshipId ? ( + noCoachingSessions + ) : ( +
+ {upcomingSessions.map((coachingSession) => ( + onUpdateSession(coachingSession)} + onDelete={() => + handleDeleteCoachingSession(coachingSession.id) + } + /> + ))} +
+ )} +
+ + {isLoadingCoachingSessions ? ( + loadingCoachingSessions + ) : isErrorCoachingSessions ? ( + errorLoadingCoachingSessions + ) : !currentCoachingRelationshipId ? ( + noCoachingSessions + ) : ( +
+ {previousSessions.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..b39f292d 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 isValidSession = sessionCookie?.value; // 4. Redirect to / if the user is not authenticated // 4b. TODO: Check session validity/expiration? - if (isProtectedRoute && !session) { + if (isProtectedRoute && !isValidSession) { return NextResponse.redirect(new URL("/", req.nextUrl)); } // 5. Redirect to /dashboard if the user is authenticated if ( isPublicRoute && - session && + isValidSession && !req.nextUrl.pathname.startsWith("/dashboard") ) { return NextResponse.redirect(new URL("/dashboard", req.nextUrl)); diff --git a/src/types/coaching-session.ts b/src/types/coaching-session.ts index 348f09ae..a29baf2a 100644 --- a/src/types/coaching-session.ts +++ b/src/types/coaching-session.ts @@ -32,24 +32,41 @@ 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 getCoachingSessionById(