Add Body Map question component — builder, mobile response & coach view#54
Add Body Map question component — builder, mobile response & coach view#54lvaheykitman wants to merge 4 commits into
Conversation
- New BlastMessageDrawer with 3-level MLS club drill-down picker - Squads / Custom Groups / stub modes with mode dropdown - 30 MLS clubs, Atlanta United FC full First Team roster (25 players) - Send via channel chips (In-app, Email, SMS) with live reach counts - Blast messages tab on Messaging page with sent/scheduled list view Co-Authored-By: Claude <noreply@anthropic.com>
- Step 1: Select recipients — Staff + Athletes sections each with expand/collapse search, checkbox list, "Select all" header, "Selected (N)" pill, and dark-avatar selected state with remove button - Step 2: Compose — channel toggles, conditional subject field, message body + SMS character counter, Add schedule (title + date range) - Added atlNamedStaff (12 named ATL staff) and atlFirstTeamFlat exports Co-Authored-By: Claude <noreply@anthropic.com>
…on flow Builds out the broadcast messaging flow on top of the V1 baseline: - Recipient picker with Clubs entry, 4 team-level selectors (Athletes / Staff / Guardians / Custom Groups), per-list team filter + A–Z/Z–A/Recent sort, and Create-custom-group modal. - Compose step: filled channel chips (renamed In-app → Broadcast Channel), attachments via paperclip + chips, mandatory subject when Email selected, schedule with date / time / repeat + Add schedule control. - Confirmation modal before send + success snackbar (top-right, slide in, Cancel/×, auto-dismiss). - Broadcast history grid replaces the card list — Upcoming / Past tabs, Subject / Scheduled date / Status chip / Channel / Recipients (with hover tooltip + +N overflow) / three-dot menu. View-mode drawer for sent messages, edit drawer for scheduled/recurring with scope dialog. - Seed data updated with realistic Upcoming + Past rows incl. recurring + sent-immediately examples. Co-Authored-By: Claude <noreply@anthropic.com>
Implements the Body Map question type across multiple surfaces and iterations: - Desktop form builder + preview (BodyMapPage, V2–V6): question config (instructions, body display, symptom types, custom question), inline multi-select symptom panel with per-symptom sliders, notes, area-grouped chips, symptom colours/layers, and clear controls. - Mobile player-facing response (BodyMapMobileResponse, V3–V10): iPhone-framed single-column flow, bottom sheet with snap points, hotspot, notes, and WhatsApp-style chip row. - Coach-facing read-only completed response view (BodyMapResponseView, V2–V3) with an AI summary panel (Anthropic API) and symptom layer toggle. - Interactive body model SVG with per-symptom colour fills by severity. Each iteration is preserved on its own route under /body-map and /body-map-mobile so versions can be compared side by side. Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a comprehensive body map feature, including SVG assets, builder configuration pages, mobile response views, and read-only response views across multiple iterations. It also adds a blast messaging system with recipient selection, scheduling, and custom group management. The review feedback highlights critical security vulnerabilities where the Anthropic API key is exposed on the client side in BodyMapResponseViewV2 and BodyMapResponseViewV3. Additionally, timezone offset bugs were identified in BlastMessageDrawer and BroadcastDetailDrawer when setting minimum dates and formatting local date-times.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| async function generateBodyMapSummary(response) { | ||
| const apiKey = import.meta.env.VITE_ANTHROPIC_API_KEY | ||
| if (!apiKey) throw new Error('Anthropic API key not configured') |
There was a problem hiding this comment.
Exposing the Anthropic API key (VITE_ANTHROPIC_API_KEY) on the client side is a critical security risk. Since Vite embeds VITE_ environment variables directly into the production JS bundle, anyone can inspect the network requests or source code to extract and abuse your API key. To resolve this, move the Anthropic API call to a secure backend endpoint or serverless function, and have the client fetch from that endpoint instead.
async function generateBodyMapSummary(response) {
const res = await fetch("/api/generate-summary", {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({ response }),
})
if (!res.ok) throw new Error("API error: " + res.status)
const data = await res.json()
return data.summary
}| async function generateBodyMapSummary(response) { | ||
| const apiKey = import.meta.env.VITE_ANTHROPIC_API_KEY | ||
| if (!apiKey) throw new Error('Anthropic API key not configured') |
There was a problem hiding this comment.
Exposing the Anthropic API key (VITE_ANTHROPIC_API_KEY) on the client side is a critical security risk. Since Vite embeds VITE_ environment variables directly into the production JS bundle, anyone can inspect the network requests or source code to extract and abuse your API key. To resolve this, move the Anthropic API call to a secure backend endpoint or serverless function, and have the client fetch from that endpoint instead.
async function generateBodyMapSummary(response) {
const res = await fetch("/api/generate-summary", {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({ response }),
})
if (!res.ok) throw new Error("API error: " + res.status)
const data = await res.json()
return data.summary
}| <TextField variant="filled" size="small" type="date" label="Start date" value={startDate} onChange={(e) => onStartDateChange(e.target.value)} InputLabelProps={{ shrink: true }} inputProps={{ min: new Date().toISOString().split('T')[0] }} sx={{ flex: 1 }} /> | ||
| <Typography variant="body2" color="text.disabled">—</Typography> | ||
| <TextField variant="filled" size="small" type="date" label="End date" value={endDate} onChange={(e) => onEndDateChange(e.target.value)} InputLabelProps={{ shrink: true }} inputProps={{ min: startDate || new Date().toISOString().split('T')[0] }} sx={{ flex: 1 }} /> |
There was a problem hiding this comment.
Using new Date().toISOString().split('T')[0] to set the minimum date can cause timezone offset bugs. Since .toISOString() returns the date in UTC, it may represent yesterday or tomorrow relative to the user's local timezone (e.g., if a user in UTC-5 is accessing the app late in the evening, the UTC date will already be the next day, making "today" an invalid selection).
Using new Date().toLocaleDateString('en-CA') is a robust and concise way to get the current date formatted as YYYY-MM-DD in the user's local timezone.
| <TextField variant="filled" size="small" type="date" label="Start date" value={startDate} onChange={(e) => onStartDateChange(e.target.value)} InputLabelProps={{ shrink: true }} inputProps={{ min: new Date().toISOString().split('T')[0] }} sx={{ flex: 1 }} /> | |
| <Typography variant="body2" color="text.disabled">—</Typography> | |
| <TextField variant="filled" size="small" type="date" label="End date" value={endDate} onChange={(e) => onEndDateChange(e.target.value)} InputLabelProps={{ shrink: true }} inputProps={{ min: startDate || new Date().toISOString().split('T')[0] }} sx={{ flex: 1 }} /> | |
| <TextField variant="filled" size="small" type="date" label="Start date" value={startDate} onChange={(e) => onStartDateChange(e.target.value)} InputLabelProps={{ shrink: true }} inputProps={{ min: new Date().toLocaleDateString('en-CA') }} sx={{ flex: 1 }} /> | |
| <Typography variant="body2" color="text.disabled">—</Typography> | |
| <TextField variant="filled" size="small" type="date" label="End date" value={endDate} onChange={(e) => onEndDateChange(e.target.value)} InputLabelProps={{ shrink: true }} inputProps={{ min: startDate || new Date().toLocaleDateString('en-CA') }} sx={{ flex: 1 }} /> |
| <TextField | ||
| variant="filled" size="small" fullWidth type="datetime-local" | ||
| label={isRecurring ? 'Next occurrence' : 'Scheduled date & time'} | ||
| value={scheduledFor ? new Date(scheduledFor).toISOString().slice(0, 16) : ''} |
There was a problem hiding this comment.
Using .toISOString().slice(0, 16) directly on a UTC date string to populate a datetime-local input causes a timezone shift. The datetime-local input expects the value in local time format (YYYY-MM-DDTHH:mm), but .toISOString() returns it in UTC. This causes the input to display the wrong time (shifted by the timezone offset) and subsequently save an incorrect time back to the server.
Adjusting the date by the local timezone offset before converting to ISO string ensures the correct local time is displayed.
| value={scheduledFor ? new Date(scheduledFor).toISOString().slice(0, 16) : ''} | |
| value={scheduledFor ? new Date(new Date(scheduledFor).getTime() - new Date(scheduledFor).getTimezoneOffset() * 60000).toISOString().slice(0, 16) : ''} |
kittwebhook
left a comment
There was a problem hiding this comment.
Hi @lvaheykitman, can you please make sure to link a Jira issue on this PR?
|
I wasn't able to update Design_Prototyping_Kit#54 - check if the bots team has write access to it? |
|
Hi @lvaheykitman, can you please make sure to link a Jira issue on this PR? |
What changed
Adds a Body Map question component across three surfaces, each iteration preserved on its own route for side-by-side comparison.
Desktop form builder + preview (
/body-map, V2–V6)Mobile player-facing response (
/body-map-mobile, V3–V10)Coach-facing completed response view (
/body-map/response, V1–V3)Shared
Notes for reviewers
/body-map/v6,/body-map-mobile/v10,/body-map/response/v3.VITE_ANTHROPIC_API_KEY; without it the panel shows the documented "Summary unavailable" fallback and still renders the data-driven list.Generated with Claude Code