Skip to content

Profile Popup Modal#87

Open
saharshmaloo wants to merge 10 commits into
mainfrom
feature/camper_modal
Open

Profile Popup Modal#87
saharshmaloo wants to merge 10 commits into
mainfrom
feature/camper_modal

Conversation

@saharshmaloo
Copy link
Copy Markdown
Collaborator

@saharshmaloo saharshmaloo commented Nov 17, 2025

This is modal that pops up when you click on each individual camper after clicking on the camper name in the activity modal. This is a low fidelity design, so focus more on getting the functionality working and the general look of it. It does NOT need to be 100% faithful to this deisgn since it will change.

It should consist of their: —> accept this data as props as it will likely be fetched higher up in the component tree

  • Name
  • Bunk and (NAV/OCP)
  • Swim Level
  • Emergency Medications
  • And names of people they have conflicts with

Summary by CodeRabbit

  • New Features
    • Added a camper details modal for viewing and editing camper profiles. Users can manage bunk assignments, swim level, health information, and conflict notes.
    • Modal supports read-only view and an edit mode with save and cancel controls; edits sync with the displayed camper data.

✏️ Tip: You can customize this high-level summary in your review settings.

- Updated the bunk field format in exampleCamper.
- Enhanced CamperModal to support edit mode with state management for edited camper details.
- Added input fields for bunk, swim level, emergency medication, and conflicts in edit mode.
- Improved layout and styling for better user experience.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Nov 17, 2025

📝 Walkthrough

Walkthrough

Adds a CamperModal React component for viewing/editing camper fields (bunk, swim level, healthInfo, conflicts) with local edit state and an openCamperModal helper. Extends CamperAttendee with an optional healthInfo?: string field.

Changes

Cohort / File(s) Summary
New Modal Component
src/components/CamperModal.tsx
Adds CamperModal (default export) with local editedCamper state, field handlers, edit/save/cancel controls, UI fields: Bunk (NumberInput), Swim Level (NumberInput), Health Info (TextInput), Conflicts (Textarea). Adds openCamperModal(props) helper to open via Mantine modals.
Type Extension
src/types/sessionTypes.ts
Adds optional healthInfo?: string to CamperAttendee type.

Sequence Diagram(s)

(Skipped — changes add a UI modal component and a type extension without a multi-component control-flow requiring a sequence diagram.)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

  • Profile Pop-Up #80 — Implements the camper profile pop-up UI requested (bunk, swim level, health info/emergency meds, conflicts), matching the issue's feature scope.

Suggested reviewers

  • eshavignesh123

Poem

🐇
I popped a modal bright and neat,
For bunks and swims and health to meet,
Edit, save, or cancel play,
Camper notes tucked safe away,
Hopping off — the form's complete!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Profile Popup Modal' is partially related to the changeset. It refers to a modal component being added, which is accurate, but does not capture the primary change: adding a CamperModal component with inline edit capabilities for camper details.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 59a2d4f and a911156.

📒 Files selected for processing (1)
  • src/components/CamperModal.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/CamperModal.tsx

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (2)
src/components/CamperPopupModal.tsx (2)

97-103: Use "CANCEL" instead of "X" for clarity.

The "X" button label is confusing since there's already a close icon (crossIcon) in the top-left. Using "CANCEL" would make it clearer that this button discards edits, not closes the modal.

Apply this diff:

                 <button
                   onClick={handleCancel}
                   className="px-3 text-camp-text-headingBody underline"
                 >
-                  X
+                  CANCEL
                 </button>

115-162: Standardize label styling for consistency.

The field labels have inconsistent styling: "BUNK:" is italicized (line 117) while "Swim Level:", "Emergency Medication:", and "Conflicts:" are not. This appears unintentional.

Either add italic to all labels or remove it from "BUNK:". For consistency, apply this diff:

               <div>
-                <span className="font-semibold text-camp-text-headingBody italic">BUNK:</span>{" "}
+                <span className="font-semibold text-camp-text-headingBody">BUNK:</span>{" "}

Or add italic to the other labels if that's the intended style.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 01a9747 and 3644c15.

⛔ Files ignored due to path filters (1)
  • src/assets/icons/avatarIcon.png is excluded by !**/*.png
📒 Files selected for processing (1)
  • src/components/CamperPopupModal.tsx (1 hunks)

Comment thread src/components/CamperPopupModal.tsx Outdated
Comment on lines +8 to +15
type CamperDetails = {
name: string;
bunk: string;
navStatus: "NAV" | "OCP" | string;
swimLevel: string;
emergencyMedication?: string;
conflicts: string[];
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Display the navStatus field as required by PR objectives.

The navStatus field is defined in the type but never displayed in the component. According to the PR objectives, the modal must include "Bunk and (NAV/OCP)" information. Add the navStatus display alongside the bunk information.

Additionally, the type "NAV" | "OCP" | string is redundant since string already covers all possible values. Simplify to just string or remove the trailing | string to enforce only "NAV" or "OCP":

-    navStatus: "NAV" | "OCP" | string;
+    navStatus: "NAV" | "OCP";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
type CamperDetails = {
name: string;
bunk: string;
navStatus: "NAV" | "OCP" | string;
swimLevel: string;
emergencyMedication?: string;
conflicts: string[];
}
type CamperDetails = {
name: string;
bunk: string;
navStatus: "NAV" | "OCP";
swimLevel: string;
emergencyMedication?: string;
conflicts: string[];
}
🤖 Prompt for AI Agents
In src/components/CamperPopupModal.tsx around lines 8 to 15, the CamperDetails
type defines navStatus but the component doesn't render it and the type
currently uses the redundant `"NAV" | "OCP" | string`; update the type to either
just string or to `"NAV" | "OCP"` (choose enforcing union if you want to
restrict values), and modify the modal JSX to display bunk and navStatus
together (e.g., "Bunk: {bunk} ({navStatus})") so the UI shows the required "Bunk
and (NAV/OCP)" information.

Comment thread src/components/CamperPopupModal.tsx Outdated
Comment on lines +33 to +36
// Update edited camper when camper prop changes
useEffect(() => {
setEditedCamper(camper);
}, [camper]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Prevent data loss by checking edit mode before syncing state.

The useEffect will overwrite editedCamper even when the user is actively editing, causing data loss if the parent component re-renders with a new camper reference.

Apply this diff to only sync when not in edit mode:

     // Update edited camper when camper prop changes
     useEffect(() => {
-        setEditedCamper(camper);
-    }, [camper]);
+        if (!isEditMode) {
+            setEditedCamper(camper);
+        }
+    }, [camper, isEditMode]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Update edited camper when camper prop changes
useEffect(() => {
setEditedCamper(camper);
}, [camper]);
// Update edited camper when camper prop changes
useEffect(() => {
if (!isEditMode) {
setEditedCamper(camper);
}
}, [camper, isEditMode]);
🤖 Prompt for AI Agents
In src/components/CamperPopupModal.tsx around lines 33-36, the useEffect
currently always syncs setEditedCamper(camper) on camper changes which
overwrites in-progress edits; change the effect to only setEditedCamper when the
component is NOT in edit mode by checking an edit flag (e.g., isEditing or
isEditMode) before calling setEditedCamper, add that flag to the effect
dependency array, and if there is no existing flag add a local state to track
whether the user has started editing (set it in your input change handlers) so
parent re-renders won’t overwrite in-progress edits.

Comment thread src/components/CamperPopupModal.tsx Outdated
Comment on lines +47 to +50
const handleSave = () => {
// TODO: Add save logic here (e.g., API call)
setIsEditMode(false);
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Implement save logic to persist changes.

The handleSave function exits edit mode without persisting the changes. All edits are lost since editedCamper is never communicated back to the parent component. You need a callback prop (e.g., onSave) to pass the updated camper data to the parent.

Add an onSave prop to CamperModalProps and call it with the edited data:

 type CamperModalProps = {
     camper: CamperDetails;
     isOpen: boolean;
     onClose: () => void;
     onEdit?: () => void;
+    onSave?: (updatedCamper: CamperDetails) => void;
 };

Then update handleSave:

 const handleSave = () => {
-    // TODO: Add save logic here (e.g., API call)
+    if (onSave) {
+        onSave(editedCamper);
+    }
     setIsEditMode(false);
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleSave = () => {
// TODO: Add save logic here (e.g., API call)
setIsEditMode(false);
};
const handleSave = () => {
if (onSave) {
onSave(editedCamper);
}
setIsEditMode(false);
};
🤖 Prompt for AI Agents
In src/components/CamperPopupModal.tsx around lines 47-50, handleSave currently
only toggles edit mode and loses edits because editedCamper is never persisted;
add an onSave callback to CamperModalProps (with appropriate typing, e.g.,
(camper: Camper) => void), call props.onSave(editedCamper) inside handleSave
before setIsEditMode(false), and update any parent usage to pass a handler that
persists the camper (API call/state update) so edits are communicated back to
the parent.

Comment thread src/components/CamperPopupModal.tsx Outdated
Comment on lines +72 to +180
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 px-4">
<div className="relative w-full max-w-md bg-white p-6 text-camp-text-headingBody">
<button
onClick={onClose}
className="absolute left-4 top-4"
>
<Image src={crossIcon} alt="Close" width={20} height={20} />
</button>

<div className="absolute left-1/2 top-4 flex -translate-x-1/2 items-center gap-2">
<div className="relative h-8 w-8">
<Image src={avatarIcon} alt="Camper avatar"/>
</div>
<h2 className="text-xl font-semibold">{name}</h2>
</div>

{!isEditMode ? (
<button
onClick={handleEditClick}
className="absolute right-4 top-3 px-3 text-camp-primary underline"
>
EDIT
</button>
) : (
<div className="absolute right-4 top-3 flex">
<button
onClick={handleCancel}
className="px-3 text-camp-text-headingBody underline"
>
X
</button>
<span className="text-camp-text-headingBody">|</span>
<button
onClick={handleSave}
className="px-3 text-camp-primary underline font-semibold"
>
SAVE
</button>
</div>
)}


<section className="mt-10 text-sm font-semibold">
<div>
<span className="font-semibold text-camp-text-headingBody italic">BUNK:</span>{" "}
{isEditMode ? (
<input
type="text"
value={editedCamper.bunk}
onChange={(e) => handleFieldChange("bunk", e.target.value)}
className="border border-gray-300 px-2 py-1 rounded"
/>
) : (
<span>{bunk}</span>
)}
</div>
<div>
<span className="font-semibold text-camp-text-headingBody italic">Swim Level:</span>{" "}
{isEditMode ? (
<input
type="text"
value={editedCamper.swimLevel}
onChange={(e) => handleFieldChange("swimLevel", e.target.value)}
className="border border-gray-300 px-2 py-1 rounded"
/>
) : (
<span>{swimLevel}</span>
)}
</div>

<div>
<span className="font-semibold text-camp-text-headingBody">
Emergency Medication:
</span>{" "}
{isEditMode ? (
<input
type="text"
value={editedCamper.emergencyMedication || ""}
onChange={(e) => handleFieldChange("emergencyMedication", e.target.value)}
className="border border-gray-300 px-2 py-1 rounded"
/>
) : (
<span>{emergencyMedication?.trim() || "None reported"}</span>
)}
</div>

<div className="flex items-start gap-3">
<span className="font-semibold text-camp-text-headingBody mt-1">
Conflicts:
</span>

{isEditMode ? (
<textarea
value={editedCamper.conflicts.join(", ")}
onChange={(e) => handleFieldChange("conflicts", e.target.value.split(", ").filter(c => c.trim()))}
className="border border-black bg-gray-100 p-3 text-sm text-camp-text-headingBody italic flex-1 rounded"
rows={3}
/>
) : (
<div className="border border-black bg-gray-100 p-3 text-sm text-camp-text-headingBody italic flex-1">
{conflicts.length > 0 ? conflicts.join(", ") : "None"}
</div>
)}
</div>
</section>
</div>
</div>
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Enhance modal accessibility with keyboard support and ARIA attributes.

The modal lacks several accessibility features that are standard for modal dialogs:

  1. No ESC key handler to close the modal
  2. No focus trap (keyboard users can tab to background elements)
  3. Missing ARIA attributes (role="dialog", aria-modal="true", aria-labelledby)

Add keyboard support:

+    useEffect(() => {
+        const handleEsc = (e: KeyboardEvent) => {
+            if (e.key === 'Escape' && isOpen) {
+                if (isEditMode) {
+                    handleCancel();
+                } else {
+                    onClose();
+                }
+            }
+        };
+        document.addEventListener('keydown', handleEsc);
+        return () => document.removeEventListener('keydown', handleEsc);
+    }, [isOpen, isEditMode]);

Add ARIA attributes to the modal container:

-        <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 px-4">
-          <div className="relative w-full max-w-md bg-white p-6 text-camp-text-headingBody">
+        <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 px-4" role="dialog" aria-modal="true">
+          <div className="relative w-full max-w-md bg-white p-6 text-camp-text-headingBody" role="document">

For a complete solution, consider using a library like react-focus-lock for proper focus trapping.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 px-4">
<div className="relative w-full max-w-md bg-white p-6 text-camp-text-headingBody">
<button
onClick={onClose}
className="absolute left-4 top-4"
>
<Image src={crossIcon} alt="Close" width={20} height={20} />
</button>
<div className="absolute left-1/2 top-4 flex -translate-x-1/2 items-center gap-2">
<div className="relative h-8 w-8">
<Image src={avatarIcon} alt="Camper avatar"/>
</div>
<h2 className="text-xl font-semibold">{name}</h2>
</div>
{!isEditMode ? (
<button
onClick={handleEditClick}
className="absolute right-4 top-3 px-3 text-camp-primary underline"
>
EDIT
</button>
) : (
<div className="absolute right-4 top-3 flex">
<button
onClick={handleCancel}
className="px-3 text-camp-text-headingBody underline"
>
X
</button>
<span className="text-camp-text-headingBody">|</span>
<button
onClick={handleSave}
className="px-3 text-camp-primary underline font-semibold"
>
SAVE
</button>
</div>
)}
<section className="mt-10 text-sm font-semibold">
<div>
<span className="font-semibold text-camp-text-headingBody italic">BUNK:</span>{" "}
{isEditMode ? (
<input
type="text"
value={editedCamper.bunk}
onChange={(e) => handleFieldChange("bunk", e.target.value)}
className="border border-gray-300 px-2 py-1 rounded"
/>
) : (
<span>{bunk}</span>
)}
</div>
<div>
<span className="font-semibold text-camp-text-headingBody italic">Swim Level:</span>{" "}
{isEditMode ? (
<input
type="text"
value={editedCamper.swimLevel}
onChange={(e) => handleFieldChange("swimLevel", e.target.value)}
className="border border-gray-300 px-2 py-1 rounded"
/>
) : (
<span>{swimLevel}</span>
)}
</div>
<div>
<span className="font-semibold text-camp-text-headingBody">
Emergency Medication:
</span>{" "}
{isEditMode ? (
<input
type="text"
value={editedCamper.emergencyMedication || ""}
onChange={(e) => handleFieldChange("emergencyMedication", e.target.value)}
className="border border-gray-300 px-2 py-1 rounded"
/>
) : (
<span>{emergencyMedication?.trim() || "None reported"}</span>
)}
</div>
<div className="flex items-start gap-3">
<span className="font-semibold text-camp-text-headingBody mt-1">
Conflicts:
</span>
{isEditMode ? (
<textarea
value={editedCamper.conflicts.join(", ")}
onChange={(e) => handleFieldChange("conflicts", e.target.value.split(", ").filter(c => c.trim()))}
className="border border-black bg-gray-100 p-3 text-sm text-camp-text-headingBody italic flex-1 rounded"
rows={3}
/>
) : (
<div className="border border-black bg-gray-100 p-3 text-sm text-camp-text-headingBody italic flex-1">
{conflicts.length > 0 ? conflicts.join(", ") : "None"}
</div>
)}
</div>
</section>
</div>
</div>
);
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 px-4" role="dialog" aria-modal="true">
<div className="relative w-full max-w-md bg-white p-6 text-camp-text-headingBody" role="document">
<button
onClick={onClose}
className="absolute left-4 top-4"
>
<Image src={crossIcon} alt="Close" width={20} height={20} />
</button>
<div className="absolute left-1/2 top-4 flex -translate-x-1/2 items-center gap-2">
<div className="relative h-8 w-8">
<Image src={avatarIcon} alt="Camper avatar"/>
</div>
<h2 className="text-xl font-semibold">{name}</h2>
</div>
{!isEditMode ? (
<button
onClick={handleEditClick}
className="absolute right-4 top-3 px-3 text-camp-primary underline"
>
EDIT
</button>
) : (
<div className="absolute right-4 top-3 flex">
<button
onClick={handleCancel}
className="px-3 text-camp-text-headingBody underline"
>
X
</button>
<span className="text-camp-text-headingBody">|</span>
<button
onClick={handleSave}
className="px-3 text-camp-primary underline font-semibold"
>
SAVE
</button>
</div>
)}
<section className="mt-10 text-sm font-semibold">
<div>
<span className="font-semibold text-camp-text-headingBody italic">BUNK:</span>{" "}
{isEditMode ? (
<input
type="text"
value={editedCamper.bunk}
onChange={(e) => handleFieldChange("bunk", e.target.value)}
className="border border-gray-300 px-2 py-1 rounded"
/>
) : (
<span>{bunk}</span>
)}
</div>
<div>
<span className="font-semibold text-camp-text-headingBody italic">Swim Level:</span>{" "}
{isEditMode ? (
<input
type="text"
value={editedCamper.swimLevel}
onChange={(e) => handleFieldChange("swimLevel", e.target.value)}
className="border border-gray-300 px-2 py-1 rounded"
/>
) : (
<span>{swimLevel}</span>
)}
</div>
<div>
<span className="font-semibold text-camp-text-headingBody">
Emergency Medication:
</span>{" "}
{isEditMode ? (
<input
type="text"
value={editedCamper.emergencyMedication || ""}
onChange={(e) => handleFieldChange("emergencyMedication", e.target.value)}
className="border border-gray-300 px-2 py-1 rounded"
/>
) : (
<span>{emergencyMedication?.trim() || "None reported"}</span>
)}
</div>
<div className="flex items-start gap-3">
<span className="font-semibold text-camp-text-headingBody mt-1">
Conflicts:
</span>
{isEditMode ? (
<textarea
value={editedCamper.conflicts.join(", ")}
onChange={(e) => handleFieldChange("conflicts", e.target.value.split(", ").filter(c => c.trim()))}
className="border border-black bg-gray-100 p-3 text-sm text-camp-text-headingBody italic flex-1 rounded"
rows={3}
/>
) : (
<div className="border border-black bg-gray-100 p-3 text-sm text-camp-text-headingBody italic flex-1">
{conflicts.length > 0 ? conflicts.join(", ") : "None"}
</div>
)}
</div>
</section>
</div>
</div>
);
🤖 Prompt for AI Agents
In src/components/CamperPopupModal.tsx around lines 72-180, the modal lacks
keyboard accessibility and ARIA attributes; add role="dialog", aria-modal="true"
and aria-labelledby to the modal container and give the title h2 an id (e.g.
camper-modal-title). Implement an ESC key handler inside a useEffect that
listens for keydown and calls onClose, cleaning up the listener on unmount. Add
focus management: save document.activeElement before open, set focus to the
first focusable element (or the close button) when the modal mounts, and restore
focus on unmount; for robust tab trapping, wrap the modal content with a
focus-trap (e.g. install/use react-focus-lock or react-focus-trap) to prevent
tabbing to background elements. Ensure all event listeners and focus restoration
are properly cleaned up in the effect cleanup.

Comment thread src/components/CamperPopupModal.tsx Outdated
Comment on lines +82 to +86
<div className="absolute left-1/2 top-4 flex -translate-x-1/2 items-center gap-2">
<div className="relative h-8 w-8">
<Image src={avatarIcon} alt="Camper avatar"/>
</div>
<h2 className="text-xl font-semibold">{name}</h2>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Add required width and height props to the Image component.

The Next.js Image component requires explicit dimensions via width and height props (or the fill prop with a positioned parent). Without these, the image may not render correctly.

Apply this diff:

             <div className="absolute left-1/2 top-4 flex -translate-x-1/2 items-center gap-2">
               <div className="relative h-8 w-8">
-                <Image src={avatarIcon} alt="Camper avatar"/>
+                <Image src={avatarIcon} alt="Camper avatar" width={32} height={32} />
               </div>
               <h2 className="text-xl font-semibold">{name}</h2>
             </div>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="absolute left-1/2 top-4 flex -translate-x-1/2 items-center gap-2">
<div className="relative h-8 w-8">
<Image src={avatarIcon} alt="Camper avatar"/>
</div>
<h2 className="text-xl font-semibold">{name}</h2>
<div className="absolute left-1/2 top-4 flex -translate-x-1/2 items-center gap-2">
<div className="relative h-8 w-8">
<Image src={avatarIcon} alt="Camper avatar" width={32} height={32} />
</div>
<h2 className="text-xl font-semibold">{name}</h2>
🤖 Prompt for AI Agents
In src/components/CamperPopupModal.tsx around lines 82 to 86, the Next.js Image
is missing required size props; add explicit width and height (or use fill with
a positioned parent). Fix by giving the Image component width={32} and
height={32} to match the surrounding h-8 w-8 container (or alternatively replace
with fill and ensure the parent has position:relative and explicit size).

Comment thread src/components/CamperPopupModal.tsx Outdated
Comment on lines +164 to +176
{isEditMode ? (
<textarea
value={editedCamper.conflicts.join(", ")}
onChange={(e) => handleFieldChange("conflicts", e.target.value.split(", ").filter(c => c.trim()))}
className="border border-black bg-gray-100 p-3 text-sm text-camp-text-headingBody italic flex-1 rounded"
rows={3}
/>
) : (
<div className="border border-black bg-gray-100 p-3 text-sm text-camp-text-headingBody italic flex-1">
{conflicts.length > 0 ? conflicts.join(", ") : "None"}
</div>
)}
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Improve conflicts parsing to handle inconsistent spacing.

The current logic splits on ", " (comma with space), which breaks if users type commas without spaces or with inconsistent spacing. Additionally, individual conflict names should be trimmed.

Apply this diff to handle various spacing patterns:

                 {isEditMode ? (
                   <textarea
                     value={editedCamper.conflicts.join(", ")}
-                    onChange={(e) => handleFieldChange("conflicts", e.target.value.split(", ").filter(c => c.trim()))}
+                    onChange={(e) => handleFieldChange("conflicts", e.target.value.split(",").map(c => c.trim()).filter(c => c))}
                     className="border border-black bg-gray-100 p-3 text-sm text-camp-text-headingBody italic flex-1 rounded"
                     rows={3}
                   />

This splits on any comma, trims each item, and filters out empty strings.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{isEditMode ? (
<textarea
value={editedCamper.conflicts.join(", ")}
onChange={(e) => handleFieldChange("conflicts", e.target.value.split(", ").filter(c => c.trim()))}
className="border border-black bg-gray-100 p-3 text-sm text-camp-text-headingBody italic flex-1 rounded"
rows={3}
/>
) : (
<div className="border border-black bg-gray-100 p-3 text-sm text-camp-text-headingBody italic flex-1">
{conflicts.length > 0 ? conflicts.join(", ") : "None"}
</div>
)}
</div>
{isEditMode ? (
<textarea
value={editedCamper.conflicts.join(", ")}
onChange={(e) => handleFieldChange("conflicts", e.target.value.split(",").map(c => c.trim()).filter(c => c))}
className="border border-black bg-gray-100 p-3 text-sm text-camp-text-headingBody italic flex-1 rounded"
rows={3}
/>
) : (
<div className="border border-black bg-gray-100 p-3 text-sm text-camp-text-headingBody italic flex-1">
{conflicts.length > 0 ? conflicts.join(", ") : "None"}
</div>
)}
</div>
🤖 Prompt for AI Agents
In src/components/CamperPopupModal.tsx around lines 164 to 176, the textarea
splitting currently uses ", " which fails on inconsistent spacing; change the
onChange handler to split on any comma, trim each entry, and filter out empty
strings (e.g. split by "," then map trim then filter(Boolean)), and ensure the
textarea value still displays conflicts joined with ", " for consistent
presentation.

@nkanchinadam nkanchinadam force-pushed the main branch 2 times, most recently from a884ab9 to 974f469 Compare December 2, 2025 05:43
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In @src/components/CamperModal.tsx:
- Around line 89-94: The Health Info field currently shows sensitive PII in
plain text; restrict access and mask it in read-only mode. Update CamperModal to
check user permissions (e.g., currentUser.roles or hasPermission) before
rendering or enabling edit of healthInfo, keep the existing handleFieldChange
for edits but prevent changes unless permitted, and render a masked value (e.g.,
asterisks) when !isEditMode with a "Show" toggle that only reveals the real
healthInfo after verifying permission; additionally, emit an audit/event log
whenever healthInfo is viewed or revealed (use your existing audit/logging
utility) so access is recorded.
- Around line 49-51: The destructured nonoList from the ternary (const { bunk,
level, healthInfo, nonoList } = isEditMode ? editedCamper : camper) is unused
and the Conflicts field still reads editedCamper.nonoList directly; fix by
making the data source consistent: either remove nonoList from the destructuring
and keep using editedCamper.nonoList, or (preferred) change the Conflicts
rendering to use the destructured nonoList variable so bunk, level and
healthInfo are handled the same way as nonoList when switching between
isEditMode, editedCamper and camper.
🧹 Nitpick comments (3)
src/components/CamperModal.tsx (3)

29-32: TODO: Implement save logic.

The save functionality is currently stubbed. You'll need to add the API call to persist changes to the backend.

Do you want me to help generate the save implementation, or would you like me to open a tracking issue for this task?


95-106: Improve conflicts parsing logic.

The current implementation has two issues:

  1. Line 102: Splitting by ", " (comma-space) fails if the user omits the space (e.g., "John,Mary" won't split correctly)
  2. Line 102: The filter condition c.trim() is truthy checking, but it's clearer to explicitly check for non-empty strings
♻️ Proposed improvements
        <Textarea
          label="Conflicts"
          value={nonoList.join(", ")}
          disabled={!isEditMode}
          onChange={(e) =>
            handleFieldChange(
              "nonoList",
-             e.target.value.split(", ").filter((c) => c.trim())
+             e.target.value.split(",").map(c => c.trim()).filter((c) => c !== '')
            )
          }
          rows={3}
        />

This change:

  • Splits by comma only (works with or without spaces)
  • Trims whitespace from each name
  • Explicitly filters empty strings

77-88: Consider adding validation for numeric inputs.

The Bunk and Swim Level fields currently accept any numeric value without validation. Consider adding constraints:

  • Bunk number: likely has a valid range (e.g., min: 1, max: based on session capacity)
  • Swim level: likely has a valid range (e.g., min: 1, max: 5)

Mantine's NumberInput supports min and max props for built-in validation.

Example validation
        <NumberInput
          label="Bunk"
          value={bunk}
+         min={1}
+         max={20}
          disabled={!isEditMode}
          onChange={(value) => handleFieldChange("bunk", value)}
        />
        <NumberInput
          label="Swim Level"
          value={level}
+         min={1}
+         max={5}
          disabled={!isEditMode}
          onChange={(value) => handleFieldChange("level", value)}
        />
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3644c15 and 59a2d4f.

📒 Files selected for processing (2)
  • src/components/CamperModal.tsx
  • src/types/sessionTypes.ts
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/CamperModal.tsx (2)
src/types/sessionTypes.ts (1)
  • CamperAttendeeID (27-27)
src/utils/personUtils.ts (1)
  • getFullName (3-5)
🪛 GitHub Actions: Lint & Build on PR
src/components/CamperModal.tsx

[error] 49-49: ESLint error: 'nonoList' is assigned a value but never used. (no-unused-vars)

🔇 Additional comments (4)
src/components/CamperModal.tsx (4)

20-27: LGTM!

The state initialization and synchronization logic is well-structured. The useEffect properly syncs editedCamper with incoming prop changes, which is essential for controlled editing behavior.


39-47: LGTM!

The generic field change handler is type-safe and provides a clean abstraction for updating multiple field types.


112-117: LGTM!

The openCamperModal helper provides a clean API for opening the modal and follows Mantine's recommended patterns.


61-71: The color props "success" and "error" are valid for this project. The custom theme defined in src/styles/theme.ts explicitly includes these colors as custom theme keys. Mantine ActionIcon accepts any color key defined in the theme override, so no changes are needed.

Comment thread src/components/CamperModal.tsx Outdated
Comment on lines +89 to +94
<TextInput
label="Health Info"
value={healthInfo}
disabled={!isEditMode}
onChange={(e) => handleFieldChange("healthInfo", e.target.value)}
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Consider access controls and data masking for health information.

The Health Info field displays sensitive PII in plain text without masking. Consider:

  • Implementing role-based access controls to restrict who can view/edit health information
  • Masking the field value in read-only mode (e.g., showing asterisks or "***" with a "Show" button)
  • Adding an audit log for health info access

This complements the compliance concerns raised in sessionTypes.ts regarding the storage of this data.

🤖 Prompt for AI Agents
In @src/components/CamperModal.tsx around lines 89 - 94, The Health Info field
currently shows sensitive PII in plain text; restrict access and mask it in
read-only mode. Update CamperModal to check user permissions (e.g.,
currentUser.roles or hasPermission) before rendering or enabling edit of
healthInfo, keep the existing handleFieldChange for edits but prevent changes
unless permitted, and render a masked value (e.g., asterisks) when !isEditMode
with a "Show" toggle that only reveals the real healthInfo after verifying
permission; additionally, emit an audit/event log whenever healthInfo is viewed
or revealed (use your existing audit/logging utility) so access is recorded.

Comment thread src/types/sessionTypes.ts
level: number;
bunk: number;
swimOptOut: boolean;
healthInfo?: string;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Verify privacy and compliance requirements for health data.

The healthInfo field stores sensitive personal health information (PII). Ensure that:

  • This data is not logged in application logs, error traces, or analytics
  • Proper access controls and encryption are in place
  • Compliance requirements (HIPAA, GDPR, CCPA, etc.) for handling health information are satisfied
  • Consider adding documentation noting that this field contains PHI/PII

Based on learnings and best practices, health information requires special handling to avoid privacy violations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants