Skip to content

Commit

Permalink
Adds the OverarchingGoal.title to the CoachingSessionSelector compone…
Browse files Browse the repository at this point in the history
…nt for more context in choosing the right session to join.
  • Loading branch information
jhodapp committed Dec 29, 2024
1 parent 5737e20 commit 8067381
Show file tree
Hide file tree
Showing 6 changed files with 348 additions and 46 deletions.
146 changes: 111 additions & 35 deletions src/components/ui/coaching-session-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import {
} from "@/components/ui/select";
import { getDateTimeFromString, Id } from "@/types/general";
import { useCoachingSessions } from "@/lib/api/coaching-sessions";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { DateTime } from "ts-luxon";
import { useCoachingSessionStateStore } from "@/lib/providers/coaching-session-state-store-provider";
import { fetchOverarchingGoalsByCoachingSessionId } from "@/lib/api/overarching-goals";
import { OverarchingGoal } from "@/types/overarching-goal";

interface CoachingSessionsSelectorProps extends PopoverProps {
/// The CoachingRelationship Id for which to get a list of associated CoachingSessions
Expand All @@ -30,25 +32,47 @@ function CoachingSessionsSelectItems({
}: {
relationshipId: Id;
}) {
const { coachingSessions, isLoading, isError } =
useCoachingSessions(relationshipId);
const {
coachingSessions,
isLoading: isLoadingSessions,
isError: isErrorSessions,
} = useCoachingSessions(relationshipId);

const { setCurrentCoachingSessions } = useCoachingSessionStateStore(
(state) => state
);
const [goals, setGoals] = useState<(OverarchingGoal[] | undefined)[]>([]);
const [isLoadingGoals, setIsLoadingGoals] = useState(false);

console.debug(`coachingSessions: ${JSON.stringify(coachingSessions)}`);

// Be sure to cache the list of current coaching sessions in the CoachingSessionStateStore
useEffect(() => {
if (!coachingSessions.length) return;
console.debug(
`coachingSessions (useEffect): ${JSON.stringify(coachingSessions)}`
);
setCurrentCoachingSessions(coachingSessions);
}, [coachingSessions]);

if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error loading coaching sessions</div>;
useEffect(() => {
const fetchGoals = async () => {
setIsLoadingGoals(true);
try {
const sessionIds = coachingSessions?.map((session) => session.id) || [];
const goalsPromises = sessionIds.map((id) =>
fetchOverarchingGoalsByCoachingSessionId(id)
);
const fetchedGoals = await Promise.all(goalsPromises);
setGoals(fetchedGoals);
} catch (error) {
console.error("Error fetching goals:", error);
} finally {
setIsLoadingGoals(false);
}
};

if (coachingSessions?.length) {
fetchGoals();
}
}, [coachingSessions]);

if (isLoadingSessions || isLoadingGoals) return <div>Loading...</div>;
if (isErrorSessions) return <div>Error loading coaching sessions</div>;
if (!coachingSessions?.length) return <div>No coaching sessions found</div>;

return (
Expand All @@ -62,13 +86,25 @@ function CoachingSessionsSelectItems({
.filter(
(session) => getDateTimeFromString(session.date) < DateTime.now()
)
.map((session) => (
<SelectItem value={session.id} key={session.id}>
{getDateTimeFromString(session.date).toLocaleString(
DateTime.DATETIME_FULL
)}
</SelectItem>
))}
.map((session) => {
const sessionIndex = coachingSessions.findIndex(
(s) => s.id === session.id
);
return (
<SelectItem value={session.id} key={session.id}>
<div className="flex flex-col w-full">
<span className="text-left truncate overflow-hidden">
{goals[sessionIndex]?.[0]?.title || "No goal set"}
</span>
<span className="text-sm text-gray-400">
{getDateTimeFromString(session.date).toLocaleString(
DateTime.DATETIME_FULL
)}
</span>
</div>
</SelectItem>
);
})}
</SelectGroup>
)}
{coachingSessions.some(
Expand All @@ -80,13 +116,25 @@ function CoachingSessionsSelectItems({
.filter(
(session) => getDateTimeFromString(session.date) >= DateTime.now()
)
.map((session) => (
<SelectItem value={session.id} key={session.id}>
{getDateTimeFromString(session.date).toLocaleString(
DateTime.DATETIME_FULL
)}
</SelectItem>
))}
.map((session) => {
const sessionIndex = coachingSessions.findIndex(
(s) => s.id === session.id
);
return (
<SelectItem value={session.id} key={session.id}>
<div className="flex flex-col w-full">
<span className="text-left truncate overflow-hidden">
{goals[sessionIndex]?.[0]?.title || "No goal set"}
</span>
<span className="text-sm text-gray-400">
{getDateTimeFromString(session.date).toLocaleString(
DateTime.DATETIME_FULL
)}
</span>
</div>
</SelectItem>
);
})}
</SelectGroup>
)}
</>
Expand All @@ -105,23 +153,51 @@ export default function CoachingSessionSelector({
getCurrentCoachingSession,
} = useCoachingSessionStateStore((state) => state);

const [currentGoal, setCurrentGoal] = useState<OverarchingGoal | undefined>();
const [isLoadingGoal, setIsLoadingGoal] = useState(false);

const currentSession = currentCoachingSessionId
? getCurrentCoachingSession(currentCoachingSessionId)
: null;

useEffect(() => {
const fetchGoal = async () => {
if (!currentCoachingSessionId) return;

setIsLoadingGoal(true);
try {
const goals = await fetchOverarchingGoalsByCoachingSessionId(
currentCoachingSessionId
);
setCurrentGoal(goals[0]);
} catch (error) {
console.error("Error fetching goal:", error);
} finally {
setIsLoadingGoal(false);
}
};

fetchGoal();
}, [currentCoachingSessionId]);

const handleSetCoachingSession = (coachingSessionId: Id) => {
setCurrentCoachingSessionId(coachingSessionId);
if (onSelect) {
onSelect(relationshipId);
onSelect(coachingSessionId);
}
};

const currentSession = currentCoachingSessionId
? getCurrentCoachingSession(currentCoachingSessionId)
: null;

const displayValue = currentSession ? (
<>
{getDateTimeFromString(currentSession.date).toLocaleString(
DateTime.DATETIME_FULL
)}
</>
<div className="flex flex-col w-[28rem]">
<span className="truncate overflow-hidden text-left">
{currentGoal?.title || "No goal set"}
</span>
<span className="text-sm text-gray-500 text-left">
{getDateTimeFromString(currentSession.date).toLocaleString(
DateTime.DATETIME_FULL
)}
</span>
</div>
) : undefined;

return (
Expand Down
96 changes: 95 additions & 1 deletion src/lib/api/overarching-goals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,102 @@ import {
parseOverarchingGoal,
} from "@/types/overarching-goal";
import { ItemStatus, Id } from "@/types/general";
import { AxiosError, AxiosResponse } from "axios";
import axios, { AxiosError, AxiosResponse } from "axios";
import { siteConfig } from "@/site.config";
import useSWR, { useSWRConfig } from "swr";

interface ApiResponseOverarchingGoals {
status_code: number;
data: OverarchingGoal[];
}

// Fetch all OverarchingGoals associated with a particular User
const fetcherOverarchingGoals = async (
url: string,
coachingSessionId: Id
): Promise<OverarchingGoal[]> =>
axios
.get<ApiResponseOverarchingGoals>(url, {
params: {
coaching_session_id: coachingSessionId,
},
withCredentials: true,
timeout: 5000,
headers: {
"X-Version": siteConfig.env.backendApiVersion,
},
})
.then((res) => res.data.data);

/// A hook to retrieve all OverarchingGoals associated with coachingSessionId
export function useOverarchingGoals(coachingSessionId: Id) {
const { data, error, isLoading } = useSWR<OverarchingGoal[]>(
[
`${siteConfig.env.backendServiceURL}/overarching_goals`,
coachingSessionId,
],
([url, _token]) => fetcherOverarchingGoals(url, coachingSessionId)
);
const swrConfig = useSWRConfig();
console.debug(`swrConfig: ${JSON.stringify(swrConfig)}`);

console.debug(`overarchingGoals data: ${JSON.stringify(data)}`);

return {
overarchingGoals: Array.isArray(data) ? data : [],
isLoading,
isError: error,
};
}

/// A hook to retrieve a single OverarchingGoal by a coachingSessionId
export function useOverarchingGoalByCoachingSessionId(coachingSessionId: Id) {
const { overarchingGoals, isLoading, isError } =
useOverarchingGoals(coachingSessionId);

return {
overarchingGoal: overarchingGoals.length
? overarchingGoals[0]
: defaultOverarchingGoal(),
isLoading,
isError: isError,
};
}

interface ApiResponseOverarchingGoal {
status_code: number;
data: OverarchingGoal;
}

// Fetcher for retrieving a single OverarchingGoal by its Id
const fetcherOverarchingGoal = async (url: string): Promise<OverarchingGoal> =>
axios
.get<ApiResponseOverarchingGoal>(url, {
withCredentials: true,
timeout: 5000,
headers: {
"X-Version": siteConfig.env.backendApiVersion,
},
})
.then((res) => res.data.data);

/// A hook to retrieve a single OverarchingGoal by its Id
export function useOverarchingGoal(overarchingGoalId: Id) {
const { data, error, isLoading } = useSWR<OverarchingGoal>(
`${siteConfig.env.backendServiceURL}/overarching_goals/${overarchingGoalId}`,
fetcherOverarchingGoal
);
const swrConfig = useSWRConfig();
console.debug(`swrConfig: ${JSON.stringify(swrConfig)}`);

console.debug(`overarchingGoal data: ${JSON.stringify(data)}`);

return {
overarchingGoal: data || defaultOverarchingGoal(),
isLoading,
isError: error,
};
}

export const fetchOverarchingGoalsByCoachingSessionId = async (
coachingSessionId: Id
Expand Down
47 changes: 47 additions & 0 deletions src/lib/providers/overarching-goal-state-store-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// The purpose of this provider is to provide compatibility with
// Next.js re-rendering and component caching
"use client";

import { type ReactNode, createContext, useRef, useContext } from "react";
import { type StoreApi, useStore } from "zustand";

import {
type OverarchingGoalStateStore,
createOverarchingGoalStateStore,
} from "@/lib/stores/overarching-goal-state-store";

export const OverarchingGoalStateStoreContext =
createContext<StoreApi<OverarchingGoalStateStore> | null>(null);

export interface OverarchingGoalStateStoreProviderProps {
children: ReactNode;
}

export const OverarchingGoalStateStoreProvider = ({
children,
}: OverarchingGoalStateStoreProviderProps) => {
const storeRef = useRef<StoreApi<OverarchingGoalStateStore>>(undefined);
if (!storeRef.current) {
storeRef.current = createOverarchingGoalStateStore();
}

return (
<OverarchingGoalStateStoreContext.Provider value={storeRef.current}>
{children}
</OverarchingGoalStateStoreContext.Provider>
);
};

export const useOverarchingGoalStateStore = <T,>(
selector: (store: OverarchingGoalStateStore) => T
): T => {
const oagStateStoreContext = useContext(OverarchingGoalStateStoreContext);

if (!oagStateStoreContext) {
throw new Error(
`useOverarchingGoalStateStore must be used within OverarchingGoalStateStoreProvider`
);
}

return useStore(oagStateStoreContext, selector);
};
21 changes: 12 additions & 9 deletions src/lib/providers/root-layout-providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { SWRConfig } from "swr";
import { OrganizationStateStoreProvider } from "./organization-state-store-provider";
import { CoachingRelationshipStateStoreProvider } from "./coaching-relationship-state-store-provider";
import { CoachingSessionStateStoreProvider } from "./coaching-session-state-store-provider";
import { OverarchingGoalStateStoreProvider } from "./overarching-goal-state-store-provider";

export function RootLayoutProviders({
children,
Expand All @@ -24,15 +25,17 @@ export function RootLayoutProviders({
<OrganizationStateStoreProvider>
<CoachingRelationshipStateStoreProvider>
<CoachingSessionStateStoreProvider>
<SWRConfig
value={{
revalidateIfStale: true,
focusThrottleInterval: 10000,
provider: () => new Map(),
}}
>
{children}
</SWRConfig>
<OverarchingGoalStateStoreProvider>
<SWRConfig
value={{
revalidateIfStale: true,
focusThrottleInterval: 10000,
provider: () => new Map(),
}}
>
{children}
</SWRConfig>
</OverarchingGoalStateStoreProvider>
</CoachingSessionStateStoreProvider>
</CoachingRelationshipStateStoreProvider>
</OrganizationStateStoreProvider>
Expand Down
Loading

0 comments on commit 8067381

Please sign in to comment.