diff --git a/package-lock.json b/package-lock.json
index d7d260c..ea33398 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -38,6 +38,7 @@
"react": "^18",
"react-day-picker": "^8.10.0",
"react-dom": "^18",
+ "swr": "^2.2.5",
"tailwind-merge": "^2.2.0",
"tailwindcss-animate": "^1.0.7",
"ts-luxon": "^4.5.2",
@@ -6035,6 +6036,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/swr": {
+ "version": "2.2.5",
+ "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.5.tgz",
+ "integrity": "sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==",
+ "license": "MIT",
+ "dependencies": {
+ "client-only": "^0.0.1",
+ "use-sync-external-store": "^1.2.0"
+ },
+ "peerDependencies": {
+ "react": "^16.11.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/tailwind-merge": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.3.0.tgz",
diff --git a/package.json b/package.json
index c539ea3..133720e 100644
--- a/package.json
+++ b/package.json
@@ -39,6 +39,7 @@
"react": "^18",
"react-day-picker": "^8.10.0",
"react-dom": "^18",
+ "swr": "^2.2.5",
"tailwind-merge": "^2.2.0",
"tailwindcss-animate": "^1.0.7",
"ts-luxon": "^4.5.2",
diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx
index 00ab6c4..3062dc3 100644
--- a/src/app/dashboard/page.tsx
+++ b/src/app/dashboard/page.tsx
@@ -8,7 +8,7 @@ import Image from "next/image";
import { cn } from "@/lib/utils";
-import { SelectCoachingSession } from "@/components/ui/dashboard/select-coaching-session";
+import { JoinCoachingSession } from "@/components/ui/dashboard/join-coaching-session";
import { useAuthStore } from "@/lib/providers/auth-store-provider";
// export const metadata: Metadata = {
@@ -54,7 +54,7 @@ export default function DashboardPage() {
-
+
>
diff --git a/src/components/ui/dashboard/dynamic-api-select.tsx b/src/components/ui/dashboard/dynamic-api-select.tsx
new file mode 100644
index 0000000..d99480d
--- /dev/null
+++ b/src/components/ui/dashboard/dynamic-api-select.tsx
@@ -0,0 +1,150 @@
+import React, { useState } from "react";
+import { useApiData } from "@/hooks/use-api-data";
+import {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectLabel,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import {
+ CoachingSession,
+ isCoachingSession,
+ sortCoachingSessionArray,
+} from "@/types/coaching-session";
+import { DateTime } from "ts-luxon";
+import { SortOrder } from "@/types/general";
+
+interface DynamicApiSelectProps {
+ url: string;
+ method?: "GET" | "POST";
+ params?: Record;
+ onChange: (value: string) => void;
+ placeholder?: string;
+ getOptionLabel: (item: T) => string;
+ getOptionValue: (item: T) => string; // TODO: return generic type
+ elementId: string;
+ groupByDate?: boolean;
+}
+
+interface ApiResponse {
+ status_code: number;
+ data: T[];
+}
+
+export function DynamicApiSelect({
+ url,
+ method = "GET",
+ params = {},
+ onChange,
+ placeholder = "Select an option",
+ getOptionLabel,
+ getOptionValue,
+ elementId,
+ groupByDate = false,
+}: DynamicApiSelectProps) {
+ const {
+ data: response,
+ isLoading,
+ error,
+ } = useApiData>(url, { method, params });
+ const [value, setValue] = useState(""); // use AppStateStore
+
+ const handleValueChange = (newValue: string) => { // TODO: update state store
+ setValue(newValue);
+ onChange(newValue);
+ };
+
+ if (isLoading) return Loading...
;
+ if (error) return Error: {error.message}
;
+ if (!response || response.status_code !== 200)
+ return Error: Invalid response
;
+
+ const items = response.data;
+
+ const renderSessions = (
+ sessions: CoachingSession[],
+ label: string,
+ filterFn: (session: CoachingSession) => boolean,
+ sortOrder: SortOrder
+ ) => {
+ const filteredSessions = sessions.filter(filterFn);
+ const sortedSessions = sortCoachingSessionArray(
+ filteredSessions,
+ sortOrder
+ );
+
+ return (
+ sortedSessions.length > 0 && (
+
+ {label}
+ {sortedSessions.map((session) => (
+
+ {DateTime.fromISO(session.date).toLocaleString(
+ DateTime.DATETIME_FULL
+ )}
+
+ ))}
+
+ )
+ );
+ };
+
+ const renderCoachingSessions = (sessions: CoachingSession[]) => (
+
+ {sessions.length === 0 ? (
+
+ None found
+
+ ) : (
+ <>
+ {renderSessions(
+ sessions,
+ "Previous Sessions",
+ (session) => DateTime.fromISO(session.date) < DateTime.now(),
+ SortOrder.Descending
+ )}
+ {renderSessions(
+ sessions,
+ "Upcoming Sessions",
+ (session) => DateTime.fromISO(session.date) >= DateTime.now(),
+ SortOrder.Ascending
+ )}
+ >
+ )}
+
+ );
+
+ const renderOtherItems = (items: T[]) => (
+
+ {items.length === 0 ? (
+
+ None found
+
+ ) : (
+ items.map((item, index) => (
+
+ {getOptionLabel(item)}
+
+ ))
+ )}
+
+ );
+
+ const coachingSessions = groupByDate
+ ? (items.filter(isCoachingSession) as CoachingSession[])
+ : [];
+
+ return (
+
+ );
+}
diff --git a/src/components/ui/dashboard/join-coaching-session.tsx b/src/components/ui/dashboard/join-coaching-session.tsx
new file mode 100644
index 0000000..ac164ae
--- /dev/null
+++ b/src/components/ui/dashboard/join-coaching-session.tsx
@@ -0,0 +1,133 @@
+import React from "react";
+import { useAppStateStore } from "@/lib/providers/app-state-store-provider";
+import { Id } from "@/types/general";
+import { DynamicApiSelect } from "./dynamic-api-select";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Organization } from "@/types/organization";
+import { CoachingRelationshipWithUserNames } from "@/types/coaching_relationship_with_user_names";
+import {
+ CoachingSession,
+} from "@/types/coaching-session";
+import { DateTime } from "ts-luxon";
+import { Label } from "@/components/ui/label";
+import { Button } from "../button";
+import Link from "next/link";
+
+export interface CoachingSessionCardProps {
+ userId: Id;
+}
+
+export function JoinCoachingSession({
+ userId: userId,
+}: CoachingSessionCardProps) {
+
+ const FROM_DATE = DateTime.now().minus({ month: 1 }).toISODate();
+ const TO_DATE = DateTime.now().plus({ month: 1 }).toISODate();
+
+ const {
+ organization,
+ organizationId,
+ relationship,
+ relationshipId,
+ coachingSession,
+ coachingSessionId,
+ setOrganizationId,
+ setRelationshipId,
+ setCoachingSessionId
+ } = useAppStateStore(state => ({
+ organization: state.setOrganization,
+ organizationId: state.organizationId,
+ relationship: state.setCoachingRelationship,
+ relationshipId: state.relationshipId,
+ coachingSession: state.setCoachingSession,
+ coachingSessionId: state.coachingSessionId,
+ setOrganizationId: state.setOrganizationId,
+ setRelationshipId: state.setRelationshipId,
+ setCoachingSessionId: state.setCoachingSessionId
+ }));
+
+ // TODO: pass Organization type
+ const handleOrganizationSelection = (value: string) => {
+ setOrganizationId(value);
+ }
+
+ // TODO: pass Relationship type
+ const handleRelationshipSelection = (value: string) => {
+ setRelationshipId(value);
+ setCoachingSessionId;
+ }
+
+ // TODO: pass CoachingSession type
+ const handleSessionSelection = (value: string) => {
+ setCoachingSessionId(value);
+ }
+
+ return (
+
+
+ Join a Coaching Session
+
+
+
+
+
+
+ url="/organizations"
+ params={{ userId }}
+ onChange={handleOrganizationSelection}
+ placeholder="Select an organization"
+ getOptionLabel={(org) => org.name}
+ getOptionValue={(org) => org.id}
+ elementId="organization-selector"
+ />
+
+ {organizationId.length > 0 && (
+
+
+
+
+ url={`/organizations/${organizationId}/coaching_relationships`}
+ params={{ organizationId: organizationId }}
+ onChange={handleRelationshipSelection}
+ placeholder="Select coaching relationship"
+ getOptionLabel={(relationship) =>
+ `${relationship.coach_first_name} ${relationship.coach_last_name} -> ${relationship.coachee_first_name} ${relationship.coach_last_name}`
+ }
+ getOptionValue={(relationship) => relationship.id}
+ elementId="relationship-selector"
+ />
+
+ )}
+ {relationshipId.length > 0 && (
+
+
+
+
+ url="/coaching_sessions"
+ params={{
+ coaching_relationship_id: relationshipId,
+ from_date: FROM_DATE,
+ to_Date: TO_DATE,
+ }}
+ onChange={handleSessionSelection}
+ placeholder="Select coaching session"
+ getOptionLabel={(session) => session.date}
+ getOptionValue={(session) => session.id}
+ elementId="session-selector"
+ groupByDate={true}
+ />
+
+ )}
+ {coachingSessionId.length > 0 && (
+
+
+
+ )}
+
+
+ );
+}
diff --git a/src/components/ui/dashboard/select-coaching-session.tsx b/src/components/ui/dashboard/select-coaching-session.tsx
deleted file mode 100644
index f22345d..0000000
--- a/src/components/ui/dashboard/select-coaching-session.tsx
+++ /dev/null
@@ -1,301 +0,0 @@
-"use client";
-
-import { Button } from "@/components/ui/button";
-import {
- Card,
- CardContent,
- CardDescription,
- CardFooter,
- CardHeader,
- CardTitle,
-} from "@/components/ui/card";
-import { Label } from "@/components/ui/label";
-import {
- Select,
- SelectContent,
- SelectGroup,
- SelectItem,
- SelectLabel,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import { fetchCoachingRelationshipsWithUserNames } from "@/lib/api/coaching-relationships";
-import { fetchCoachingSessions } from "@/lib/api/coaching-sessions";
-import { fetchOrganizationsByUserId } from "@/lib/api/organizations";
-import { useAppStateStore } from "@/lib/providers/app-state-store-provider";
-import {
- CoachingSession,
- coachingSessionToString,
- getCoachingSessionById,
-} from "@/types/coaching-session";
-import {
- CoachingRelationshipWithUserNames,
- coachingRelationshipWithUserNamesToString,
- getCoachingRelationshipById,
-} from "@/types/coaching_relationship_with_user_names";
-import { getDateTimeFromString, Id } from "@/types/general";
-import {
- getOrganizationById,
- Organization,
- organizationToString,
-} from "@/types/organization";
-import Link from "next/link";
-import { useEffect, useState } from "react";
-import { DateTime } from "ts-luxon";
-
-export interface CoachingSessionProps {
- /** The current logged in user's Id */
- userId: Id;
-}
-
-export function SelectCoachingSession({
- userId: userId,
- ...props
-}: CoachingSessionProps) {
- const { organizationId, setOrganizationId } = useAppStateStore(
- (state) => state
- );
- const { organization, setOrganization } = useAppStateStore((state) => state);
- const { relationshipId, setRelationshipId } = useAppStateStore(
- (state) => state
- );
- const { coachingRelationship, setCoachingRelationship } = useAppStateStore(
- (state) => state
- );
- const { coachingSessionId, setCoachingSessionId } = useAppStateStore(
- (state) => state
- );
- const { coachingSession, setCoachingSession } = useAppStateStore(
- (state) => state
- );
-
- const [organizations, setOrganizations] = useState([]);
- const [coachingRelationships, setCoachingRelationships] = useState<
- CoachingRelationshipWithUserNames[]
- >([]);
- const [coachingSessions, setCoachingSessions] = useState(
- []
- );
-
- useEffect(() => {
- async function loadOrganizations() {
- if (!userId) return;
-
- await fetchOrganizationsByUserId(userId)
- .then(([orgs]) => {
- // Apparently it's normal for this to be triggered twice in modern
- // React versions in strict + development modes
- // https://stackoverflow.com/questions/60618844/react-hooks-useeffect-is-called-twice-even-if-an-empty-array-is-used-as-an-ar
- console.debug("setOrganizations: " + JSON.stringify(orgs));
- setOrganizations(orgs);
- })
- .catch(([err]) => {
- console.error("Failed to fetch Organizations: " + err);
- });
- }
- loadOrganizations();
- }, [userId]);
-
- useEffect(() => {
- async function loadCoachingRelationships() {
- if (!organizationId) return;
-
- console.debug("organizationId: " + organizationId);
-
- await fetchCoachingRelationshipsWithUserNames(organizationId)
- .then(([relationships]) => {
- console.debug(
- "setCoachingRelationships: " + JSON.stringify(relationships)
- );
- setCoachingRelationships(relationships);
- })
- .catch(([err]) => {
- console.error("Failed to fetch coaching relationships: " + err);
- });
- }
- loadCoachingRelationships();
- }, [organizationId]);
-
- useEffect(() => {
- async function loadCoachingSessions() {
- if (!organizationId) return;
-
- await fetchCoachingSessions(relationshipId)
- .then(([coaching_sessions]) => {
- console.debug(
- "setCoachingSessions: " + JSON.stringify(coaching_sessions)
- );
- setCoachingSessions(coaching_sessions);
- })
- .catch(([err]) => {
- console.error("Failed to fetch coaching sessions: " + err);
- });
- }
- loadCoachingSessions();
- }, [relationshipId]);
-
- const handleSetOrganization = (organizationId: Id) => {
- setOrganizationId(organizationId);
- const organization = getOrganizationById(organizationId, organizations);
- console.debug("organization: " + organizationToString(organization));
- setOrganization(organization);
- };
-
- const handleSetCoachingRelationship = (coachingRelationshipId: string) => {
- setRelationshipId(coachingRelationshipId);
- const coachingRelationship = getCoachingRelationshipById(
- coachingRelationshipId,
- coachingRelationships
- );
- console.debug(
- "coachingRelationship: " +
- coachingRelationshipWithUserNamesToString(coachingRelationship)
- );
- setCoachingRelationship(coachingRelationship);
- };
-
- const handleSetCoachingSession = (coachingSessionId: string) => {
- setCoachingSessionId(coachingSessionId);
- const coachingSession = getCoachingSessionById(
- coachingSessionId,
- coachingSessions
- );
- console.debug(
- "coachingSession: " + coachingSessionToString(coachingSession)
- );
- setCoachingSession(coachingSession);
- };
-
- return (
-
-
- Join a Coaching Session
-
- Select current organization, relationship and session
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/src/hooks/use-api-data.ts b/src/hooks/use-api-data.ts
new file mode 100644
index 0000000..fdd4298
--- /dev/null
+++ b/src/hooks/use-api-data.ts
@@ -0,0 +1,60 @@
+import useSWR from 'swr';
+import { siteConfig } from '@/site.config';
+
+interface FetcherOptions {
+ url: string,
+ method?: 'GET' | 'POST',
+ params?: Record,
+ // body?: Record
+}
+
+const baseUrl = siteConfig.url;
+
+const fetcher = async ({ url, method = 'POST', params }: FetcherOptions) => {
+ const fullUrl = `${baseUrl}${url}`;
+
+ const headers: HeadersInit = {
+ 'Content-Type': 'application/json',
+ 'X-Version': '0.0.1'
+ };
+
+ const fetchOptions: RequestInit = {
+ method,
+ headers,
+ credentials: 'include',
+ };
+
+ const response = await fetch(fullUrl, fetchOptions);
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => null);
+ throw new Error(errorData?.message || 'An error occurred while fetching the data.');
+ }
+ return response.json();
+};
+
+export function useApiData(
+ url: string,
+ options: {
+ method?: 'GET' | 'POST'
+ params?: Record
+ body?: Record
+ } = {}
+) {
+ const { method = 'POST', params = {}, body = {} } = options
+
+ const { data, error, isLoading, mutate } = useSWR(
+ { url, method, params, body },
+ fetcher,
+ {
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ }
+ )
+
+ return {
+ data,
+ isLoading,
+ error,
+ mutate,
+ }
+}
\ No newline at end of file
diff --git a/src/lib/api/coaching-sessions.ts b/src/lib/api/coaching-sessions.ts
index 6d68490..a76647d 100644
--- a/src/lib/api/coaching-sessions.ts
+++ b/src/lib/api/coaching-sessions.ts
@@ -12,7 +12,7 @@ import { DateTime } from "ts-luxon";
export const fetchCoachingSessions = async (
coachingRelationshipId: Id
-): Promise<[CoachingSession[], string]> => {
+): Promise => {
const axios = require("axios");
var coaching_sessions: CoachingSession[] = [];
@@ -59,15 +59,9 @@ export const fetchCoachingSessions = async (
// handle error
console.error(error.response?.status);
if (error.response?.status == 401) {
- console.error("Retrieval of CoachingSessions failed: unauthorized.");
err = "Retrieval of CoachingSessions failed: unauthorized.";
} else {
console.log(error);
- console.error(
- `Retrieval of CoachingSessions by coaching relationship Id (` +
- coachingRelationshipId +
- `) failed.`
- );
err =
`Retrieval of CoachingSessions by coaching relationship Id (` +
coachingRelationshipId +
@@ -75,5 +69,7 @@ export const fetchCoachingSessions = async (
}
});
- return [coaching_sessions, err];
+ if (err) throw err;
+
+ return coaching_sessions;
};
diff --git a/src/lib/api/organizations.ts b/src/lib/api/organizations.ts
index 3cade9c..724ef9a 100644
--- a/src/lib/api/organizations.ts
+++ b/src/lib/api/organizations.ts
@@ -55,9 +55,7 @@ export const fetchOrganizations = async (): Promise<
return [organizations, err];
};
-export const fetchOrganization = async (
- id: Id
-): Promise<[Organization, string]> => {
+export const fetchOrganization = async (id: Id): Promise => {
const axios = require("axios");
var organization: Organization = defaultOrganization();
@@ -81,16 +79,15 @@ export const fetchOrganization = async (
// handle error
console.error(error.response?.status);
if (error.response?.status == 401) {
- console.error("Retrieval of Organization failed: unauthorized.");
err = "Retrieval of Organization failed: unauthorized.";
} else {
- console.log(error);
- console.error(`Retrieval of Organization(` + id + `) failed.`);
err = `Retrieval of Organization(` + id + `) failed.`;
}
});
- return [organization, err];
+ if (err) throw err;
+
+ return organization;
};
export const fetchOrganizationsByUserId = async (
diff --git a/src/lib/stores/app-state-store.ts b/src/lib/stores/app-state-store.ts
index 8ed892e..f1982a8 100644
--- a/src/lib/stores/app-state-store.ts
+++ b/src/lib/stores/app-state-store.ts
@@ -20,7 +20,7 @@ interface AppState {
coachingRelationship: CoachingRelationshipWithUserNames;
}
-interface AppStateActions {
+export interface AppStateActions {
setOrganizationId: (organizationId: Id) => void;
setRelationshipId: (relationshipId: Id) => void;
setCoachingSessionId: (coachingSessionId: Id) => void;
diff --git a/src/site.config.ts b/src/site.config.ts
index 79d560e..4465a72 100644
--- a/src/site.config.ts
+++ b/src/site.config.ts
@@ -1,6 +1,6 @@
export const siteConfig = {
name: "Refactor Coaching & Mentoring",
- url: "https://refactorcoach.com",
+ url: "http://localhost:4000",
ogImage: "https://ui.shadcn.com/og.jpg",
locale: "us",
titleStyle: SessionTitleStyle.CoachFirstCoacheeFirstDate,