Skip to content

Instantly share code, notes, and snippets.

@ManiruzzamanAkash
Created January 4, 2026 05:18
Show Gist options
  • Select an option

  • Save ManiruzzamanAkash/5907645ebd2b1341a78ac350b0678c5b to your computer and use it in GitHub Desktop.

Select an option

Save ManiruzzamanAkash/5907645ebd2b1341a78ac350b0678c5b to your computer and use it in GitHub Desktop.
import { useState, useEffect, useRef, useCallback } from "react";
import ArrowDownIcon from "../../../../IconComponents/ArrowDownIcon";
import ArrowUpIcon from "../../../../IconComponents/ArrowUpIcon";
import TabSelector from "../../../components/TabSelector";
import axios from "@utils/axios";
import { useForm } from "react-hook-form";
import { notifyError, notifySuccess } from "../../../components/Notification";
import CommentForm from "../../components/tools/CommentForm";
import TextEditorForCourse from "../../../../components/text-editor/text-editor-for-course";
import useDraftManager from "../../../../hooks/useDraftManager";
import { UnsavedChangesNotice } from "../../../../components/drafts/unsaved-changes";
import NoSessionIcon from "@components/sessions/no-session-icon";
// Update the button styles with consistent text colors
const buttonStyles = {
active: "bg-defaultPrimary text-white",
default: "bg-defaultPrimary-30 text-defaultPrimary",
hover: "hover:bg-defaultPrimary hover:text-white",
action: "transition-colors",
};
export default function ListView({
currentPage,
sessions,
lastPage,
moreHandler,
onCancelAppointment,
loadingMore,
activeTab,
onTabChange,
}) {
const [selectedSession, setSelectedSession] = useState(null);
const [sessionOpenDetails, setSessionOpenDetails] = useState(null);
const [openCoachingPlan, setOpenCoachingPlan] = useState(false);
const [openNotes, setOpenNotes] = useState(false);
const [openTools, setOpenTools] = useState(false);
const [coachNote, setCoachNote] = useState("");
const [privateNote, setPrivateNote] = useState("");
const [isCancelling, setIsCancelling] = useState(false);
const sessionsContainerRef = useRef(null);
useEffect(() => {
// Check URL parameters for session to expand
const urlParams = new URLSearchParams(window.location.search);
const sessionToExpand = urlParams.get("session");
const shouldExpand = urlParams.get("expand");
const section = urlParams.get("section");
// Reset all section states first
setOpenCoachingPlan(false);
setOpenNotes(false);
setOpenTools(false);
if (sessionToExpand && shouldExpand && sessions.length > 0) {
const session = sessions.find((s) => s.uuid === sessionToExpand);
if (session) {
setSessionOpenDetails(session.id);
setSelectedSession(session);
// Open the appropriate section based on the section parameter
switch (section) {
case "coaching_plan":
setOpenTools(false); // Close other sections
setOpenNotes(false);
setOpenCoachingPlan(true); // Open QuestionsPreview
setTimeout(() => {
document
.querySelector(".coaching-plan-section")
?.scrollIntoView({ behavior: "smooth" });
}, 100);
break;
case "notes":
setOpenTools(false); // Close other sections
setOpenCoachingPlan(false);
setOpenNotes(true); // Open NotesPreview
setTimeout(() => {
document
.querySelector(".coach-notes-section")
?.scrollIntoView({ behavior: "smooth" });
}, 100);
break;
case "tools":
setOpenCoachingPlan(false); // Close other sections
setOpenNotes(false);
setOpenTools(true); // Open ToolsPreview
setTimeout(() => {
document
.querySelector(".tools-section")
?.scrollIntoView({ behavior: "smooth" });
}, 100);
break;
}
}
}
// Cleanup function
return () => {
setOpenCoachingPlan(false);
setOpenNotes(false);
setOpenTools(false);
};
}, [sessions]);
// Replace the filteredSessions logic - we don't need to filter by date anymore
// since the backend will return the correct sessions based on activeTab
const sessionsList = sessions || [];
// If we have a session to expand, ensure all sessions are loaded
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const sessionToExpand = urlParams.get("session");
if (sessionToExpand && sessions.length > 0) {
const session = sessions.find((s) => s.uuid === sessionToExpand);
if (!session && currentPage < lastPage) {
// If we haven't found the session and there are more pages, load more
moreHandler();
}
}
}, [sessions, currentPage, lastPage]);
const statusClasses = (session) => {
return " text-gray-900";
};
const toggleSessionDetails = (event, session) => {
event.stopPropagation();
if (sessionOpenDetails === session.id) {
setSessionOpenDetails(null);
setSelectedSession(null);
} else {
setSessionOpenDetails(session.id);
setSelectedSession(session);
}
};
const submitCoachNote = async (sessionId) => {
if (!coachNote.trim()) {
notifyError("Please enter a note");
return;
}
try {
const response = await axios.post("/coach/session/notes/store", {
session_id: sessionId,
note: coachNote,
type: "coach",
});
if (response.data.status === "success") {
notifySuccess("Note added successfully");
setCoachNote("");
// Refresh session data
const updatedSession = await loadSessionData(sessionId);
setSelectedSession(updatedSession);
}
} catch (error) {
notifyError("Failed to add note");
console.error(error);
}
};
const submitPrivateNote = async (sessionId) => {
if (!privateNote.trim()) {
notifyError("Please enter a note");
return;
}
try {
const response = await axios.post("/coach/session/notes/store", {
session_id: sessionId,
note: privateNote,
type: "private",
});
if (response.data.status === "success") {
notifySuccess("Private note added successfully");
setPrivateNote("");
const updatedSession = await loadSessionData(sessionId);
setSelectedSession(updatedSession);
}
} catch (error) {
notifyError("Failed to add private note");
console.error(error);
}
};
const loadSessionData = async (sessionId) => {
try {
const response = await axios.get(
`/client/sessions/${sessionId}/data`
);
return response.data;
} catch (error) {
console.error("Failed to load session data:", error);
return null;
}
};
// Preview Components
const CoachingPlanPreview = ({ session }) => (
<div className="flex flex-col bg-white">
<button
onClick={() => setOpenCoachingPlan(!openCoachingPlan)}
className={`flex items-center justify-between w-full px-4 py-3 rounded-lg transition-all duration-200
${
openCoachingPlan
? "bg-defaultPrimary text-white shadow-sm"
: "bg-blue-50 text-defaultPrimary hover:bg-blue-100"
}`}
>
<h1 className="normal-text font-semibold text-center items-center">
Coaching plan
</h1>
{openCoachingPlan ? (
<ArrowDownIcon
className={`w-5 h-5 ${
openCoachingPlan ? "text-white" : "text-defaultPrimary"
}`}
/>
) : (
<ArrowUpIcon
className={`w-5 h-5 ${
openCoachingPlan ? "text-white" : "text-defaultPrimary"
}`}
/>
)}
</button>
{openCoachingPlan && (
<div className="mt-6 space-y-6">
{session?.coaching_plan?.questions?.map((question) => (
<div
key={question.id}
className="bg-white p-6 rounded-lg shadow-sm border border-gray-100"
>
<h1 className="normal-text font-semibold text-gray-900 mb-3">
{question.question}
</h1>
<p className="text-gray-700">
{question.answer || "No answer provided"}
</p>
</div>
))}
</div>
)}
</div>
);
const SessionCoachNote = ({ notes, className }) => (
<div className="flex flex-col bg-white">
<button
onClick={() => setOpenNotes(!openNotes)}
className={
"flex flex-row items-center justify-between w-full " +
(openNotes
? "primary-button"
: "secondary-button !bg-blue-50")
}
>
<h1
className={
"text-center w-full normal-text " +
(openNotes ? "" : "text-black")
}
>
Coach Notes
</h1>
{openNotes ? (
<ArrowDownIcon className="w-4 h-4 text-gray-600" />
) : (
<ArrowUpIcon className="w-4 h-4 text-gray-600" />
)}
</button>
{openNotes && (
<div className="flex flex-col gap-[24px] mt-[32px]">
<TextEditorForCourse
value={notes}
onChange={setCoachNote}
placeholder="Add a coach note..."
/>
<button
onClick={() => submitCoachNote(session.id)}
className={`px-6 py-3 rounded-lg normal-text font-medium w-fit ${buttonStyles.action}`}
>
Add Note
</button>
{session?.coach_notes?.map((note) => (
<div key={note.id} className="bg-gray-50 p-4 rounded">
<div
dangerouslySetInnerHTML={{ __html: note.note }}
/>
<div className="text-sm text-gray-500 mt-2">
{new Date(note.created_at).toLocaleString()}
</div>
</div>
))}
</div>
)}
</div>
);
const NotesPreview = ({ session }) => {
const [openNotes, setOpenNotes] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const [isEditing, setIsEditing] = useState(
!session.client_notes?.[0]?.note_json
);
const [notes, setNotes] = useState({
my_current_focus: "",
notes_from_session: "",
action_items_committing_to: "",
questions_arise_during_session: "",
});
// Only initialize draft managers when notes section is open
const {
updateDraft: updateCurrentFocusDraft,
clearDraft: clearCurrentFocusDraft,
isDirty: hasCurrentFocusUnsavedChanges,
} = useDraftManager(
openNotes && session?.uuid
? `client_notes_current_focus:${session.uuid}`
: null,
"",
{
autoSaveDelay: 2000,
enableLocalStorage: true,
enableServerStorage: false, // don't store this on server.
onRestored: (restoredData) => {
if (restoredData && restoredData.trim()) {
setNotes((prev) => ({
...prev,
my_current_focus: restoredData,
}));
}
},
}
);
const {
updateDraft: updateSessionNotesDraft,
clearDraft: clearSessionNotesDraft,
isDirty: hasSessionNotesUnsavedChanges,
} = useDraftManager(
openNotes && session?.uuid
? `client_notes_session_notes:${session.uuid}`
: null,
"",
{
autoSaveDelay: 2000,
enableLocalStorage: true,
enableServerStorage: false, // don't store this on server.
onRestored: (restoredData) => {
if (restoredData && restoredData.trim()) {
setNotes((prev) => ({
...prev,
notes_from_session: restoredData,
}));
}
},
}
);
const {
updateDraft: updateActionItemsDraft,
clearDraft: clearActionItemsDraft,
isDirty: hasActionItemsUnsavedChanges,
} = useDraftManager(
openNotes && session?.uuid
? `client_notes_action_items:${session.uuid}`
: null,
"",
{
autoSaveDelay: 2000,
enableLocalStorage: true,
enableServerStorage: false, // don't store this on server.
onRestored: (restoredData) => {
if (restoredData && restoredData.trim()) {
setNotes((prev) => ({
...prev,
action_items_committing_to: restoredData,
}));
}
},
}
);
const {
updateDraft: updateQuestionsDraft,
clearDraft: clearQuestionsDraft,
isDirty: hasQuestionsUnsavedChanges,
} = useDraftManager(
openNotes && session?.uuid
? `client_notes_questions:${session.uuid}`
: null,
"",
{
autoSaveDelay: 2000,
enableLocalStorage: true,
enableServerStorage: false, // don't store this on server.
onRestored: (restoredData) => {
if (restoredData && restoredData.trim()) {
setNotes((prev) => ({
...prev,
questions_arise_during_session: restoredData,
}));
}
},
}
);
useEffect(() => {
if (session.client_notes?.[0]?.note_json) {
setNotes(session.client_notes[0].note_json);
setIsEditing(false);
} else {
setIsEditing(true);
}
}, [session]);
const handleSubmit = async (field) => {
if (!notes[field]) {
notifyError("Please enter some content before saving.");
return;
}
try {
let response;
const updatedNotes = { ...notes };
const sessionUuid = session.uuid;
const isEditing = session.client_notes?.[0]?.id;
if (isEditing) {
response = await axios.put(
`/client/sessions/${sessionUuid}/notes/${session.client_notes[0].id}`,
updatedNotes
);
} else {
response = await axios.post(
`/client/sessions/${sessionUuid}/notes`,
updatedNotes
);
}
if (response.data.status === "success") {
notifySuccess("Your note has been saved successfully!");
// Clear the appropriate draft after successful save
switch (field) {
case "my_current_focus":
await clearCurrentFocusDraft();
break;
case "notes_from_session":
await clearSessionNotesDraft();
break;
case "action_items_committing_to":
await clearActionItemsDraft();
break;
case "questions_arise_during_session":
await clearQuestionsDraft();
break;
}
// If this is a new note, update the session data to include the new note
if (!isEditing && response.data.data) {
setSelectedSession((prev) => ({
...prev,
client_notes: [
response.data.data,
...(prev.client_notes || []),
],
}));
}
} else {
notifyError(
response.data.message || "Failed to save note."
);
}
} catch (error) {
console.error("Error saving notes:", error);
notifyError(
error.response?.data?.message || "Failed to save note."
);
}
};
// Handle draft updates for each field
const handleNotesChange = (field, value) => {
setNotes((prev) => ({
...prev,
[field]: value,
}));
// Only update appropriate draft if notes section is open
if (session?.uuid && openNotes) {
switch (field) {
case "my_current_focus":
updateCurrentFocusDraft(value);
break;
case "notes_from_session":
updateSessionNotesDraft(value);
break;
case "action_items_committing_to":
updateActionItemsDraft(value);
break;
case "questions_arise_during_session":
updateQuestionsDraft(value);
break;
}
}
};
// Get the appropriate isDirty flag for each field
const getUnsavedChangesFlag = (field) => {
switch (field) {
case "my_current_focus":
return hasCurrentFocusUnsavedChanges;
case "notes_from_session":
return hasSessionNotesUnsavedChanges;
case "action_items_committing_to":
return hasActionItemsUnsavedChanges;
case "questions_arise_during_session":
return hasQuestionsUnsavedChanges;
default:
return false;
}
};
const renderField = (field, label) => {
if (isEditing) {
return (
<div className="w-full mb-6">
<h1 className="normal-text font-medium text-gray-900 mb-3 flex justify-between">
{label}
{getUnsavedChangesFlag(field) && <UnsavedChangesNotice />}
</h1>
<div className="bg-white rounded-lg">
<TextEditorForCourse
value={notes[field]}
onChange={(value) =>
handleNotesChange(field, value)
}
// placeholder={`Enter your ${label.toLowerCase()}...`}
/>
</div>
<div className="flex justify-end mt-4">
<button
onClick={() => handleSubmit(field)}
className={`px-6 py-2 rounded-lg normal-text font-medium ${buttonStyles.action}`}
>
Save
</button>
</div>
</div>
);
}
return notes[field] ? (
<div className="w-full mb-6">
<h1 className="normal-text font-semibold text-gray-800 mb-4">
{label}
</h1>
<div
className="normal-text text-gray-800 leading-relaxed rendered-content"
dangerouslySetInnerHTML={{ __html: notes[field] }}
style={{
"& ul": {
listStyle: "disc",
paddingLeft: "2rem",
marginTop: "0.5rem",
marginBottom: "0.5rem",
},
"& ol": {
listStyle: "decimal",
paddingLeft: "2rem",
marginTop: "0.5rem",
marginBottom: "0.5rem",
},
"& li": { marginBottom: "0.25rem" },
"& p": { marginBottom: "0.5rem" },
}}
/>
<style jsx>{`
.rendered-content ul {
list-style: disc !important;
padding-left: 2rem !important;
margin: 0.5rem 0 !important;
}
.rendered-content ol {
list-style: decimal !important;
padding-left: 2rem !important;
margin: 0.5rem 0 !important;
}
.rendered-content li {
margin-bottom: 0.25rem !important;
display: list-item !important;
}
.rendered-content p {
margin-bottom: 0.5rem !important;
}
`}</style>
</div>
) : null;
};
return (
<div className="flex flex-col bg-white">
<button
onClick={(e) => {
e.stopPropagation();
setOpenNotes(!openNotes);
}}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className={
"flex flex-row items-center justify-between w-full px-4 py-3 rounded-lg transition-colors border border-defaultPrimary normal-text " +
(openNotes
? buttonStyles.active
: `${buttonStyles.default} ${buttonStyles.hover}`)
}
>
<h1 className={`normal-text font-semibold text-center items-center ${!openNotes && isHovered ? "text-white" : ""}`}>
My Private Notes
</h1>
{openNotes ? (
<ArrowDownIcon className="w-5 h-5 text-white" />
) : (
<ArrowUpIcon className={`w-5 h-5 ${isHovered ? "text-white" : "text-defaultPrimary"}`} />
)}
</button>
{openNotes && (
<div className="flex flex-col gap-6 mt-6">
<div className="bg-blue-50/10 p-6 rounded-xl border border-blue-100">
{isEditing ? (
<div className="space-y-4">
{renderField(
"my_current_focus",
"My current focus and priorities"
)}
{renderField(
"notes_from_session",
"Notes from our session"
)}
{renderField(
"action_items_committing_to",
"Action items I'm committing to"
)}
{renderField(
"questions_arise_during_session",
"Questions that arise during session"
)}
<div className="flex justify-end mt-4">
<button
onClick={() => setIsEditing(false)}
className="px-6 py-2 rounded-lg normal-text font-medium text-gray-700 hover:bg-gray-100 border border-gray-300 ml-4"
>
Close
</button>
</div>
</div>
) : (
<div className="space-y-3">
{renderField(
"my_current_focus",
"My current focus and priorities"
)}
{renderField(
"notes_from_session",
"Notes from our session"
)}
{renderField(
"action_items_committing_to",
"Action items I'm committing to"
)}
{renderField(
"questions_arise_during_session",
"Questions that arise during session"
)}
<button
onClick={() => setIsEditing(true)}
className={`px-6 py-3 rounded-lg normal-text font-medium ${buttonStyles.action}`}
>
Edit Notes
</button>
</div>
)}
</div>
</div>
)}
</div>
);
};
const QuestionsPreview = ({ session }) => {
const [showEntries, setShowEntries] = useState(2);
const [showCommentForm, setShowCommentForm] = useState(false);
const [questionAnswers, setQuestionAnswers] = useState({});
const [unsavedQuestions, setUnsavedQuestions] = useState(new Set()); // Track which questions have unsaved changes
const [isHovered, setIsHovered] = useState(false);
// Draft management for coaching plan questions
const {
updateDraft: updateQuestionDraft,
clearDraft: clearQuestionDraft,
} = useDraftManager(
openCoachingPlan && session?.uuid ? `coaching_plan_questions:${session.uuid}` : null,
"",
{
autoSaveDelay: 2000,
enableLocalStorage: true,
enableServerStorage: false,
onRestored: (restoredData) => {
if (restoredData && restoredData.trim()) {
try {
const parsedData = JSON.parse(restoredData);
setQuestionAnswers(parsedData);
// Mark questions with content as unsaved
const unsavedSet = new Set();
Object.entries(parsedData).forEach(([questionId, value]) => {
if (value && value.trim()) {
unsavedSet.add(parseInt(questionId));
}
});
setUnsavedQuestions(unsavedSet);
} catch (error) {
console.error("Error parsing restored question data:", error);
}
}
},
}
);
const {
register,
handleSubmit,
formState: { errors },
setValue,
} = useForm({
defaultValues: {
questions: session?.plan_details?.[0]?.questions?.reduce(
(acc, question) => {
acc[question.id] = {
id: question.id,
question: question.question,
answer: "",
};
return acc;
},
{}
) || {},
},
});
// Handle answer changes with draft saving
const handleAnswerChange = (questionId, value) => {
const updatedAnswers = {
...questionAnswers,
[questionId]: value
};
setQuestionAnswers(updatedAnswers);
setValue(`questions.${questionId}.answer`, value);
// Track which questions have unsaved changes
setUnsavedQuestions(prev => {
const newSet = new Set(prev);
if (value && value.trim()) {
newSet.add(questionId);
} else {
newSet.delete(questionId);
}
return newSet;
});
// Save to draft
if (session?.uuid && openCoachingPlan) {
updateQuestionDraft(JSON.stringify(updatedAnswers));
}
};
const hasSubmittedAnswers = session?.plan_details?.[0]?.questions?.some(
(question) =>
question.answer?.answer && question.answer?.answer !== "-"
);
const onSubmit = async (data) => {
// Ensure questions object exists and is not null/undefined
const questions = data?.questions || {};
let haveAnyAnswers =
Object.keys(questions)?.filter(
(key) => questions[key]?.answer !== ""
) || [];
if (haveAnyAnswers.length === 0) {
notifyError(
"Please answer at least one question to submit the plan."
);
return;
}
const processedData = Object.keys(questions).reduce(
(acc, key) => {
if (questions[key].answer === "") {
questions[key].answer = "-";
}
acc.push(questions[key]);
return acc;
},
[]
);
try {
const url = `/client/sessions/${session.uuid}/plan`;
const response = await axios.post(
url,
{
questions: processedData,
},
{
headers: {
"X-CSRF-TOKEN": document
.querySelector('meta[name="csrf-token"]')
.getAttribute("content"),
},
}
);
if (response.data.status === "success") {
notifySuccess(response.data.message);
// Clear draft after successful submission
await clearQuestionDraft();
// Clear unsaved changes tracking
setUnsavedQuestions(new Set());
setQuestionAnswers({});
const updatedSession = await loadSessionData(session.id);
if (updatedSession) {
setSelectedSession(updatedSession);
}
} else {
notifyError(response.data.message);
}
} catch (error) {
notifyError(
error.response?.data?.message ||
"Unknown error occurred while saving session plan"
);
console.error(error);
}
};
const submitComment = async () => {
if (!comment) {
notifyError("Please enter a comment before submitting.");
return;
}
try {
const url = `/client/sessions/${selectedSession.uuid}/comments`;
const response = await axios.post(url, {
comment,
});
if (response.data.status === "success") {
notifySuccess("Your comment has been added.");
// Create new comment object with user data from response
const newComment = {
id: response.data.id,
entry_details: response.data.comment,
formatted_created_at:
response.data.formatted_created_at,
user: response.data.user,
};
// Update local state with the new comment
setSelectedSession((prevSession) => ({
...prevSession,
plan_comments: [
newComment,
...(prevSession.plan_comments || []),
],
}));
// Clear comment input
setComment("");
} else {
notifyError(response.data.message);
}
} catch (error) {
notifyError("An error occurred while submitting the comment.");
console.error(error);
}
};
return (
<div className="flex flex-col bg-white coaching-plan-section">
<button
onClick={() => setOpenCoachingPlan(!openCoachingPlan)}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className={
"flex flex-row items-center justify-between w-full px-4 py-3 rounded-lg transition-colors border border-defaultPrimary " +
(openCoachingPlan
? buttonStyles.active
: `${buttonStyles.default} ${buttonStyles.hover}`)
}
>
<h1 className={`normal-text font-semibold text-center items-center ${!openCoachingPlan && isHovered ? "text-white" : ""}`}>
Coaching Plan
</h1>
{openCoachingPlan ? (
<ArrowDownIcon className="w-5 h-5 text-white" />
) : (
<ArrowUpIcon className={`w-5 h-5 ${isHovered ? "text-white" : "text-defaultPrimary"}`} />
)}
</button>
{openCoachingPlan && (
<div className="flex flex-col gap-6 mt-6">
{!hasSubmittedAnswers ? (
<form
onSubmit={handleSubmit(onSubmit)}
className="flex flex-col gap-6"
>
{session?.plan_details?.[0]?.questions?.map(
(question) => (
<div
key={question.id}
className="flex flex-col gap-3"
>
<h1 className="normal-text font-medium text-gray-900 flex justify-between">
{question.question}
{unsavedQuestions.has(question.id) && <UnsavedChangesNotice />}
</h1>
<textarea
{...register(
`questions.${question.id}.answer`
)}
value={questionAnswers[question.id] || ""}
onChange={(e) => handleAnswerChange(question.id, e.target.value)}
className="w-full rounded-lg border-gray-400 normal-text"
rows="5"
placeholder={`Enter your answer for: ${question.question}`}
/>
</div>
)
)}
<button
type="submit"
className={`px-6 py-3 rounded-lg normal-text font-medium w-fit ${buttonStyles.action}`}
>
Submit plan
</button>
</form>
) : (
<div className="flex flex-col gap-6">
{session?.plan_details?.[0]?.questions?.map(
(question) => (
<div
key={question.id}
className="flex flex-col gap-3"
>
<h1 className="normal-text font-medium text-gray-900">
{question.question}
</h1>
<p className="normal-text text-gray-700">
{question?.answer?.answer ||
"No answer provided"}
</p>
</div>
)
)}
</div>
)}
{/* Add Comments Section */}
<div className="mt-6">
<div className="flex items-center mb-4">
{!showCommentForm && (
<button
onClick={() => setShowCommentForm(true)}
className="flex items-center gap-2 text-defaultPrimary transition-colors normal-text font-semibold"
>
Add Comment
</button>
)}
</div>
{showCommentForm && (
<div className="mb-4">
<CommentForm
submitUrl={`/client/sessions/${selectedSession.uuid}/comments`}
closeHandler={() =>
setShowCommentForm(false)
}
onCommentAdded={(response) => {
const newComment = {
id: response.data.id,
entry_details:
response.data.comment,
formatted_created_at:
response.data
.formatted_created_at,
user: response.data.user,
};
setSelectedSession(
(prevSession) => ({
...prevSession,
plan_comments: [
newComment,
...(prevSession.plan_comments ||
[]),
],
})
);
setShowCommentForm(false);
// Keep coaching plan section open
setOpenCoachingPlan(true);
}}
/>
</div>
)}
{/* Display Comments */}
{session.plan_comments.length > 0 && (
<div className="space-y-4 sm:space-y-6">
{session.plan_comments
.slice(0, showEntries)
.map((comment, index) => (
<div
key={comment.id || index}
className="flex flex-col sm:flex-row items-start gap-1 sm:gap-3 p-3 sm:p-6 bg-blue-50 rounded-xl border border-blue-100"
>
<div className="flex-shrink-0 mb-3 sm:mb-0">
{comment.user.avatar ? (
<img
src={
comment.user
.avatar
}
alt={
comment.user
.name
}
className="w-8 h-8 sm:w-10 sm:h-10 rounded-full border-2 border-white shadow-sm"
/>
) : (
<div className="w-8 h-8 sm:w-10 sm:h-10 rounded-full bg-defaultPrimary text-white flex items-center justify-center font-medium text-lg shadow-sm">
{comment.user.name.charAt(
0
)}
</div>
)}
</div>
<div className="flex-1 min-w-0">
<div className="flex flex-col mb-2 sm:mb-3">
<h3 className="normal-text font-medium text-gray-900 mb-1 sm:mb-0">
{comment.user.name}
</h3>
<span className="instructions-text text-gray-800">
{
comment.formatted_created_at
}
</span>
</div>
<div
className="normal-text text-gray-800 leading-relaxed break-words"
dangerouslySetInnerHTML={{
__html: comment.entry_details,
}}
/>
</div>
</div>
))}
{session.plan_comments.length >
showEntries && (
<button
onClick={() =>
setShowEntries(showEntries + 2)
}
className="text-defaultPrimary hover:text-blue-700 font-medium normal-text mt-2 sm:mt-4 flex items-center gap-2 mx-auto"
>
<span>Show More Comments</span>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
)}
</div>
)}
</div>
</div>
)}
</div>
);
};
const CoachNotesPreview = ({ session }) => {
const [showCommentForm, setShowCommentForm] = useState({});
const [showEntries, setShowEntries] = useState(5);
const [isHovered, setIsHovered] = useState(false);
const onCommentAddedHandler = (response, noteId) => {
// Create new comment object with response data
const newComment = {
id: response.id,
comment: response.comment,
formatted_created_at: response.formatted_created_at,
user: {
name:
response.user?.name ||
selectedSession.client?.name ||
"Anonymous",
avatar:
response.user?.avatar || selectedSession.client?.avatar,
},
};
// Update the local state with the new comment
setSelectedSession((prevSession) => {
const updatedNotes = prevSession.coach_notes.map((n) => {
if (n.id === noteId) {
return {
...n,
comments: [newComment, ...(n.comments || [])],
};
}
return n;
});
return {
...prevSession,
coach_notes: updatedNotes,
};
});
// Only hide the comment form for this specific note
setShowCommentForm((prev) => ({
...prev,
[noteId]: false,
}));
// Keep notes section open
setOpenNotes(true);
};
return (
<div className="flex flex-col bg-white coach-notes-section">
<button
onClick={(e) => {
e.stopPropagation();
setOpenNotes(!openNotes);
}}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className={
"flex flex-row items-center justify-between w-full px-4 py-3 rounded-lg transition-colors border border-defaultPrimary " +
(openNotes
? buttonStyles.active
: `${buttonStyles.default} ${buttonStyles.hover}`)
}
>
<h1 className={`normal-text font-semibold text-center items-center ${!openNotes && isHovered ? "text-white" : ""}`}>
Coach Notes{" "}
{session?.coach_notes?.length > 0 && (
<span>({session?.coach_notes?.length || 0})</span>
)}
</h1>
{openNotes ? (
<ArrowDownIcon className="w-5 h-5 text-white" />
) : (
<ArrowUpIcon className={`w-5 h-5 ${isHovered ? "text-white" : "text-defaultPrimary"}`} />
)}
</button>
{openNotes && session?.coach_notes?.length > 0 && (
<div className="flex flex-col gap-6 mt-6">
{session.coach_notes
.slice(0, showEntries)
.map((note) => (
<div
key={note.id}
className="flex flex-col gap-4 bg-blue-50 p-6 rounded-xl border border-blue-100"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
{note.user.avatar ? (
<img
src={note.user.avatar}
alt={note.user.name}
className="w-10 h-10 rounded-full border-2 border-white shadow-sm"
/>
) : (
<div className="w-10 h-10 rounded-full bg-defaultPrimary text-white flex items-center justify-center font-medium text-lg shadow-sm">
{note.user.name.charAt(0)}
</div>
)}
<div className="flex flex-col">
<div className="normal-text font-medium text-gray-900">
{note.user.name}
</div>
<div className="instructions-text text-gray-600">
{note.formatted_created_at}
</div>
</div>
</div>
</div>
<div
className="prose max-w-none normal-text text-gray-800 leading-relaxed"
dangerouslySetInnerHTML={{
__html: note.note,
}}
/>
{!showCommentForm[note.id] && (
<button
onClick={() =>
setShowCommentForm((prev) => ({
...prev,
[note.id]: true,
}))
}
className="text-defaultPrimary hover:text-blue-700 font-medium normal-text w-max"
>
Add Comment
</button>
)}
{showCommentForm[note.id] && (
<CommentForm
submitUrl={`/comment/session/note/${note.id}`}
closeHandler={() =>
setShowCommentForm((prev) => ({
...prev,
[note.id]: false,
}))
}
onCommentAdded={(response) => {
onCommentAddedHandler(
response,
note.id
);
}}
/>
)}
{note.comments &&
note.comments.length > 0 && (
<div className="mt-4 space-y-4">
{note.comments.map(
(comment, index) => (
<div
key={
comment.id ||
index
}
className="flex items-start gap-4 bg-white p-4 rounded-lg border border-gray-100"
>
<div className="flex-shrink-0">
{comment.user
.avatar ? (
<img
src={
comment
.user
.avatar
}
alt={
comment
.user
.name
}
className="w-8 h-8 rounded-full"
/>
) : (
<div className="w-8 h-8 rounded-full bg-defaultPrimary text-white flex items-center justify-center font-medium">
{(
comment
.user
?.name ||
"Anonymous"
).charAt(
0
)}
</div>
)}
</div>
<div className="flex-1">
<div className="flex flex-col">
<span className="normal-text font-medium text-gray-900">
{comment
.user
?.name ||
"Anonymous"}
</span>
<span className="instructions-text text-gray-600">
{
comment.formatted_created_at
}
</span>
</div>
<div
className="normal-text text-gray-800 mt-2"
dangerouslySetInnerHTML={{
__html: comment.comment
}}
/>
</div>
</div>
)
)}
</div>
)}
</div>
))}
{session.coach_notes.length > showEntries && (
<button
onClick={() => setShowEntries(showEntries + 5)}
className="text-defaultPrimary hover:text-blue-700 font-medium normal-text mt-4 flex items-center gap-2"
>
<span>Show More Notes</span>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
)}
</div>
)}
{openNotes &&
(!session?.coach_notes ||
session.coach_notes.length === 0) && (
<div className="flex justify-center items-center p-6 normal-text">
No coach notes available
</div>
)}
</div>
);
};
const ToolsPreview = ({ session }) => {
const [isHovered, setIsHovered] = useState(false);
const tools = [
{
id: 1,
title: "Action Items",
icon: "/logos/action-item.svg",
items: session.action_items || [],
type: "action-item",
path: "action-items",
},
{
id: 2,
title: "Worksheets",
icon: "/logos/worksheet.svg",
items: session.worksheets || [],
type: "worksheet",
path: "worksheets",
},
{
id: 3,
title: "Goals",
icon: "/logos/goal.svg",
items: session.goals || [],
type: "goal",
path: "goals",
},
{
id: 4,
title: "Journals",
icon: "/logos/journals.png",
items: session.journals || [],
type: "journal",
path: "journals",
},
{
id: 5,
title: "Resources",
icon: "/logos/resources.png",
items: session.resources || [],
type: "resource",
path: "resources",
},
];
return (
<div className="flex flex-col bg-white tools-section">
<button
onClick={() => setOpenTools(!openTools)}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className={
"flex flex-row items-center justify-between w-full px-4 py-3 rounded-lg transition-colors border border-defaultPrimary " +
(openTools
? buttonStyles.active
: `${buttonStyles.default} ${buttonStyles.hover}`)
}
>
<h1 className={`normal-text font-semibold text-center items-center ${!openTools && isHovered ? "text-white" : ""}`}>
Tools{" "}
{tools?.length > 0 && (
<span>
(
{tools?.reduce(
(count, tool) => count + tool.items.length,
0
)}
)
</span>
)}
</h1>
{openTools ? (
<ArrowDownIcon className="w-5 h-5 text-white" />
) : (
<ArrowUpIcon className={`w-5 h-5 ${isHovered ? "text-white" : "text-defaultPrimary"}`} />
)}
</button>
{openTools && (
<div className="flex flex-col gap-6 mt-6">
{tools.map(
(tool) =>
tool.items.length > 0 && (
<div
key={tool.id}
className="bg-white rounded-lg border border-gray-400 p-6 hover:bg-blue-50"
>
<a
href={`/client/coaching/pad/tools/${tool.path}/${tool.items[0]?.client_tool_uuid}`}
className="flex items-center gap-3 mb-4 w-fit border-2 border-[#67bbf7] p-3 rounded-lg hover:bg-blue-50 transition-colors"
>
<img
src={tool.icon}
alt={tool.title}
className="w-6 h-6"
/>
<h2 className="normal-text font-medium text-gray-900">
{tool.title}
</h2>
</a>
<div className="flex flex-col gap-4">
{tool.items.map((item) => (
<a
key={item.id}
href={`/client/coaching/pad/tools/${tool.path}/${item.client_tool_uuid}`}
className="text-defaultPrimary hover:text-[#0371c0] normal-text"
>
{item.name}
</a>
))}
</div>
</div>
)
)}
</div>
)}
</div>
);
};
const SessionDetails = ({ session }) => {
// Add debug logs
console.log("Session data:", session);
console.log("Coach notes:", session.coach_notes);
console.log("Coach notes length:", session.coach_notes?.length);
console.log(
"Coach notes condition:",
session.coach_notes && session.coach_notes.length > 0
);
return (
<div className="session-details">
{/* Coach Notes Section */}
{session.coach_notes && session.coach_notes.length > 0 && (
<div className="coach-notes-section mb-6">
<h3 className="text-lg font-semibold mb-4">
Notes from Coach
</h3>
<div className="space-y-4">
{session.coach_notes.map((note) => {
// Add debug log for each note
console.log("Processing note:", note);
return (
<div
key={note.id}
className="bg-gray-50 p-4 rounded-lg"
>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center">
{note.user.avatar ? (
<img
src={note.user.avatar}
alt={note.user.name}
className="w-8 h-8 rounded-full mr-3"
/>
) : (
<div className="w-8 h-8 rounded-full bg-gray-200 flex items-center justify-center mr-3">
{note.user.name.charAt(
0
)}
</div>
)}
<div>
<div className="font-medium">
{note.user.name}
</div>
<div className="text-sm text-gray-500">
{
note.formatted_created_at
}
</div>
</div>
</div>
</div>
<div
className="prose max-w-none"
dangerouslySetInnerHTML={{
__html: note.note,
}}
/>
</div>
);
})}
</div>
</div>
)}
{/* Coaching Plan Section */}
{session.plan_details && session.plan_details.length > 0 && (
<div className="coaching-plan-section">
{/* ... existing coaching plan code ... */}
</div>
)}
</div>
);
};
// Use this for scroll handling
const handleScroll = useCallback(() => {
if (loadingMore || currentPage >= lastPage) return;
// Calculate distance from bottom of window to bottom of document
const scrollTop = window.scrollY || document.documentElement.scrollTop;
const windowHeight = window.innerHeight;
const documentHeight = Math.max(
document.body.scrollHeight,
document.body.offsetHeight,
document.documentElement.clientHeight,
document.documentElement.scrollHeight,
document.documentElement.offsetHeight
);
// Load more when user is within 200px of bottom
const distanceFromBottom = documentHeight - (scrollTop + windowHeight);
console.log(
`Scroll debug: Distance from bottom: ${distanceFromBottom}px, Loading: ${loadingMore}`
);
if (distanceFromBottom < 200 && !loadingMore && moreHandler) {
console.log("Loading more sessions from scroll trigger");
moreHandler();
}
}, [currentPage, lastPage, loadingMore, moreHandler]);
useEffect(() => {
// Debounce scroll event to improve performance
const debouncedHandleScroll = debounce(handleScroll, 100);
window.addEventListener("scroll", debouncedHandleScroll);
// Check once after render in case the page isn't long enough to scroll
setTimeout(() => handleScroll(), 100);
return () =>
window.removeEventListener("scroll", debouncedHandleScroll);
}, [handleScroll]);
// Simple debounce function to limit scroll event handling
function debounce(func, wait) {
let timeout;
return function () {
const context = this;
const args = arguments;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), wait);
}
}
// If we have a session to expand, ensure it's visible
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const sessionToExpand = urlParams.get("session");
if (sessionToExpand && sessions.length > 0) {
const session = sessions.find((s) => s.uuid === sessionToExpand);
if (!session && currentPage < lastPage && moreHandler) {
// If we haven't found the session and there are more pages, load more
moreHandler();
}
}
}, [sessions, currentPage, lastPage, moreHandler]);
return (
<div
className="flex flex-col gap-[24px] w-full"
ref={sessionsContainerRef}
>
<div className="px-0 xl:px-4">
<TabSelector
tabs={[
{
id: "upcoming",
text: "Upcoming Sessions",
handler: () => onTabChange("upcoming"),
},
{
id: "past",
text: "Past Sessions",
handler: () => onTabChange("past"),
},
]}
selectedTab={activeTab}
/>
</div>
<div className="-mx-2 sm:-mx-4">
{sessionsList.map((session) => (
<div
key={session.id}
className="flex flex-col mb-3 sm:mb-4 sm:rounded-xl rounded-none border-b sm:border border-gray-400 bg-white group overflow-hidden"
>
<div
className={`relative flex flex-col xl:flex-row justify-between items-start xl:items-center w-full cursor-pointer bg-white px-3 sm:px-6 py-3 sm:py-5 ${
sessionOpenDetails === session.id
? "bg-defaultPrimary/[0.05]"
: "hover:bg-defaultPrimary/[0.02] transition-colors duration-200"
}`}
onClick={(event) =>
toggleSessionDetails(event, session)
}
>
{/* Blue line indicator */}
<div
className={`absolute left-0 top-0 h-full w-0.5 xl:w-1 bg-defaultPrimary transition-all duration-300 ${
sessionOpenDetails === session.id
? "opacity-100"
: "opacity-70 group-hover:opacity-100"
}`}
/>
{/* Mobile/Tablet Layout */}
<div className="flex flex-col w-full xl:hidden">
<div className="flex flex-col sm:flex-row sm:items-start justify-between">
<div className="flex-1 min-w-0 mb-3 sm:mb-0">
<div className="flex items-center justify-between mb-1">
<h3 className="text-gray-900 normal-text leading-5 truncate pr-3">
{session?.coach_name}
</h3>
{(session.session_status ===
"cancelled" ||
(session.session_status ===
"active" &&
!isCancelling &&
new Date(
session.session_date_and_time
) > new Date())) && (
<div className="flex-shrink-0">
{session.session_status ===
"cancelled" ? (
<span className="inline-flex items-center px-1.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800 border border-red-200 whitespace-nowrap">
<span className="w-1 h-1 rounded-full bg-red-600 mr-1"></span>
Cancelled
</span>
) : (
<button
onClick={(e) => {
e.stopPropagation();
onCancelAppointment(session.id);
}}
className="inline-flex items-center px-1.5 py-1 sm:px-2.5 sm:py-1.5 rounded-md text-xs font-medium text-red-600 hover:text-white hover:bg-red-600 border-0 sm:border sm:border-red-600 transition-all duration-300 gap-1.5 whitespace-nowrap opacity-0 group-hover:opacity-100 hover:shadow-sm active:scale-95 hover:scale-[1.02]"
>
<svg
className="w-3 h-3 sm:w-3.5 sm:h-3.5 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
<span className="relative top-px">
Cancel
</span>
</button>
)}
</div>
)}
</div>
<p className="text-gray-600 text-sm leading-5 mb-0.5">
{`${session.date_month_year} ${
session.formatted_start_time.split(
" "
)[0]
} - ${session.formatted_end_time}`}
</p>
<p className="text-gray-500 text-sm leading-5 truncate">
{session.session_type}
</p>
</div>
</div>
</div>
{/* Desktop Layout */}
<div className="hidden xl:flex flex-1 gap-2 sm:gap-4 md:gap-6 relative z-10">
<div className="flex items-start md:items-center w-full md:w-[18%] pl-4 sm:pl-8 min-w-0">
<div className="flex flex-col min-w-0 group-hover:translate-x-1 transition-transform duration-300">
<span
className="text-gray-900 normal-text font-medium truncate"
title={session?.coach_name}
>
{session?.coach_name}
</span>
</div>
</div>
<div className="flex items-center w-full md:w-1/3 justify-start md:justify-center font-medium text-gray-800 normal-text pl-4 md:pl-0 group-hover:translate-x-1 transition-transform duration-300">
<span className="truncate text-sm sm:text-base">
{`${session.date_month_year} ${
session.formatted_start_time.split(
" "
)[0]
} - ${session.formatted_end_time}`}
</span>
</div>
<div className="flex items-center w-full md:w-1/3 pl-4 md:pl-4 pr-8 sm:pr-12 text-gray-700 normal-text group-hover:translate-x-1 transition-transform duration-300">
<div className="flex items-center justify-between w-full">
<div className="line-clamp-2 overflow-hidden text-ellipsis">
{session?.session_type}
</div>
{session.session_status ===
"cancelled" ? (
<span className="inline-flex px-2 sm:px-3 py-1 rounded-full text-xs sm:text-sm font-medium transition-all duration-300 bg-red-100 text-red-800 whitespace-nowrap ml-2">
Cancelled
</span>
) : (
session.session_status ===
"active" &&
!isCancelling &&
new Date(
session.session_date_and_time
) > new Date() && (
<button
onClick={(e) => {
e.stopPropagation();
onCancelAppointment(session.id);
}}
className="inline-flex items-center px-1.5 py-1 sm:px-2.5 sm:py-1.5 rounded-md text-xs font-medium text-red-600 hover:text-white hover:bg-red-600 border-0 sm:border sm:border-red-600 transition-all duration-300 gap-1.5 whitespace-nowrap opacity-0 group-hover:opacity-100 hover:shadow-sm active:scale-95 hover:scale-[1.02]"
disabled={isCancelling}
>
<svg
className="w-3 h-3 sm:w-3.5 sm:h-3.5 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
<span className="relative top-px">
Cancel
</span>
</button>
)
)}
</div>
</div>
</div>
{/* Arrow indicator - Adjusted for tablet */}
<div
className={`absolute right-2 sm:right-4 xl:right-6 top-1/2 -translate-y-1/2 transition-all duration-300 ${
sessionOpenDetails === session.id
? "rotate-180 text-defaultPrimary"
: "text-gray-400 group-hover:text-gray-600"
}`}
>
<svg
className="w-4 sm:w-5 h-4 sm:h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M19 9l-7 7-7-7"
/>
</svg>
</div>
</div>
{sessionOpenDetails === session.id && (
<div className="border-t border-gray-200 bg-white">
<div className="p-3 sm:p-5 xl:p-7">
<div className="flex flex-col gap-4 sm:gap-5 xl:gap-6 rounded-lg w-full">
<div className="space-y-4 sm:space-y-6 xl:space-y-8 w-full">
<div className="w-full hover:translate-x-1 transition-transform duration-300">
<CoachNotesPreview
session={selectedSession}
/>
</div>
<div className="w-full hover:translate-x-1 transition-transform duration-300">
<QuestionsPreview
session={selectedSession}
/>
</div>
<div className="w-full hover:translate-x-1 transition-transform duration-300">
<NotesPreview
session={selectedSession}
/>
</div>
<div className="w-full hover:translate-x-1 transition-transform duration-300">
<ToolsPreview
session={selectedSession}
/>
</div>
</div>
</div>
</div>
</div>
)}
</div>
))}
</div>
{/* --- START EMPTY STATE --- */}
{sessionsList.length === 0 && activeTab === "upcoming" && (
<div className="flex items-center justify-center w-full py-10 sm:py-24 bg-white rounded-none sm:rounded-2xl border-b sm:border border-gray-200 mx-0">
<div className="flex flex-col items-center gap-4 sm:gap-5 px-3 sm:px-0 max-w-md text-center">
<NoSessionIcon width={100} height={100} />
<div className="flex flex-col gap-[16px]">
<p className="normal-text font-semibold text-gray-600">
No new upcoming sessions. Book a new session!
</p>
<a
href="/client/sessions/create"
className={`btn-primary h-12 flex items-center justify-center`}
>
BOOK SESSION
</a>
</div>
<div className="flex flex-col gap-[16px] mt-[10px]">
<p className="normal-text font-semibold text-gray-600">
Looking for your past sessions? Check out the
Past Sessions tab.
</p>
<button
className={`btn-primary-outline h-12 flex items-center justify-center`}
onClick={() => onTabChange("past")}
>
PAST SESSIONS
</button>
</div>
</div>
</div>
)}
{sessionsList.length === 0 && activeTab === "past" && (
<div className="flex items-center justify-center w-full py-10 sm:py-24 bg-white rounded-none sm:rounded-2xl border-b sm:border border-gray-200 mx-0">
<div className="flex flex-col items-center gap-4 sm:gap-5 px-3 sm:px-0 max-w-md text-center">
<NoSessionIcon width={100} height={100} />
<div className="flex flex-col gap-[16px]">
<p className="normal-text font-semibold text-gray-600">
No past session found. Book a new session.
</p>
<a
href="/client/sessions/create"
className={`inline-flex items-center justify-center px-6 py-3 rounded-xl text-sm font-medium shadow-sm hover:shadow-md transition-all duration-300 ${buttonStyles.action}`}
>
BOOK SESSION
</a>
</div>
</div>
</div>
)}
{/* --- END EMPTY STATE --- */}
{/* Loading indicator - Only show if loadingMore is true */}
{/* {loadingMore && (
<div className="flex justify-center py-6">
<div className="bg-white px-6 py-4 rounded-lg shadow-sm border border-gray-200 flex items-center space-x-3">
<div className="flex items-center space-x-2">
<div
className="w-3 h-3 rounded-full bg-[#0b8ce9] animate-bounce"
style={{ animationDelay: "0.1s" }}
></div>
<div
className="w-3 h-3 rounded-full bg-[#0b8ce9] animate-bounce"
style={{ animationDelay: "0.3s" }}
></div>
<div
className="w-3 h-3 rounded-full bg-[#0b8ce9] animate-bounce"
style={{ animationDelay: "0.5s" }}
></div>
</div>
</div>
</div>
)} */}
</div>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment