feat(calculator): Runna/Strava-style UX with suggested times, VDOT badge, and time slider#63
feat(calculator): Runna/Strava-style UX with suggested times, VDOT badge, and time slider#63
Conversation
…dge, and time slider - Add SUGGESTED_TIMES: contextual goal-time chips per distance preset (e.g. "Sub 3h", "3:30", "4:00" for Marathon) that instantly populate the HH:MM:SS fields and auto-calculate training paces on tap - Add SLIDER_RANGES: per-distance min/max/step config for the fine-tune slider (Marathon 2h–7h, 5K 12min–45min, etc.) - Add live VDOT badge (⚡ VDOT 45.2 · Intermediate) that updates as the user types or drags the slider – powered by Jack Daniels' formula - Add fine-tune slider that appears after a preset distance is chosen and a valid time is entered; dragging updates HH/MM/SS fields in real time - Auto-advance focus HH → MM → SS after 2 digits for faster entry - Auto-calculate when a suggested-time chip is tapped (no button press needed) - Track selectedPresetName in PaceCalculatorV2 so chips + slider are contextually shown only for known race distances https://claude.ai/code/session_011ty7rQtakAjWbks15dqSXE
… unchanged Notes April 2026 changes: SUGGESTED_TIMES chips, SLIDER_RANGES fine-tune slider, live VDOT badge, auto-advance focus, updated onPresetClick signature. Explicitly records that the save flow (handleSave → SavePlanDialog → saveToDashboard → Firestore, incl. guest-redirect) is unchanged. https://claude.ai/code/session_011ty7rQtakAjWbks15dqSXE
✅ Deploy Preview for trainpace ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| UnusedCode | 3 medium |
| ErrorProne | 3 medium 7 high |
| Security | 3 high |
| CodeStyle | 1 minor |
🟢 Metrics 45 complexity · 1 duplication
Metric Results Complexity 45 Duplication 1
TIP This summary will be updated as you push new changes. Give us feedback
There was a problem hiding this comment.
Pull request overview
Updates the Pace Calculator UI/UX to a more “Runna/Strava-style” workflow by adding contextual suggested-time chips, a fine-tune time slider, and a live VDOT badge that updates as inputs change.
Changes:
- Add
SUGGESTED_TIMESandSLIDER_RANGESconstants to drive distance-specific chips and slider behavior. - Enhance
RaceDetailsFormto render suggested chips, HH/MM/SS auto-advance, a conditional slider, and a live VDOT badge. - Update
PaceCalculatorV2to trackselectedPresetName, computeliveVdot, and support auto-calc on chip selection and slider-driven time updates.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| vite-project/src/features/pace-calculator/types.ts | Adds suggested-time and slider-range configuration per preset distance. |
| vite-project/src/features/pace-calculator/components/RaceDetailsForm.tsx | Implements the new UX elements (chips, slider, VDOT badge, auto-advance). |
| vite-project/src/features/pace-calculator/components/PaceCalculatorV2.tsx | Adds state/handlers for preset tracking, live VDOT, chip auto-calc, and slider time updates. |
| CLAUDE.md | Documents the new UX features and updated prop signature. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <div | ||
| className="relative w-32 h-10 bg-blue-100 rounded-full cursor-pointer overflow-hidden flex-shrink-0" | ||
| onClick={handleUnitToggle} | ||
| > |
There was a problem hiding this comment.
The unit toggle is implemented as a clickable
| {/* ── 4. Fine-tune slider (preset distances only, after time is entered) ── */} | ||
| {sliderRange && hasValidTime && ( |
There was a problem hiding this comment.
The fine-tune slider is shown when currentTimeSeconds > 0, but this includes invalid times (e.g. minutes/seconds >= 60). This contradicts the “valid time exists” behavior and can show a slider for an invalid input state. Consider gating on the same validity rules as validatePaceInputs (e.g. hide when errors.time is set, or explicitly check minutes/seconds < 60).
| {/* ── 4. Fine-tune slider (preset distances only, after time is entered) ── */} | |
| {sliderRange && hasValidTime && ( | |
| {/* ── 4. Fine-tune slider (preset distances only, after a valid time is entered) ── */} | |
| {sliderRange && hasValidTime && !errors.time && ( |
| const handleInputChange = (e: { target: { name: string; value: string } }) => { | ||
| const { name, value } = e.target; | ||
|
|
||
| // Time input validation | ||
| if (["hours", "minutes", "seconds"].includes(name)) { | ||
| const numValue = value.replace(/\D/g, ""); | ||
| setInputs((prev) => ({ | ||
| ...prev, | ||
| [name]: numValue.slice(0, 2), | ||
| })); | ||
| setInputs((prev) => ({ ...prev, [name]: numValue.slice(0, 2) })); | ||
| return; | ||
| } | ||
|
|
||
| setInputs((prev) => ({ | ||
| ...prev, | ||
| [name]: value, | ||
| })); | ||
| setInputs((prev) => ({ ...prev, [name]: value })); | ||
|
|
||
| // Clear errors for this field | ||
| if (errors[name as keyof FormErrors]) { | ||
| setErrors((prev) => ({ | ||
| ...prev, | ||
| [name]: undefined, | ||
| })); | ||
| setErrors((prev) => ({ ...prev, [name]: undefined })); | ||
| } | ||
| }; |
There was a problem hiding this comment.
selectedPresetName is only set when a preset button is clicked, but it is never cleared when the user manually edits the distance field. This can leave suggested-time chips/slider showing for a preset even after switching to a custom distance, which conflicts with “contextually shown only for known race distances.” Clear selectedPresetName when name === "distance" changes via typing/paste (and possibly when units toggle changes distance away from the preset).
| const totalSecs = timeToSeconds(inputs.hours, inputs.minutes, inputs.seconds); | ||
| if (totalSecs <= 0) return null; | ||
| const distMeters = inputs.units === "km" ? dist * 1000 : dist * 1609.34; | ||
| const vdot = calculateVdot(distMeters, totalSecs); | ||
| if (!isFinite(vdot) || vdot < 10 || vdot > 100) return null; | ||
| return Math.round(vdot * 10) / 10; | ||
| }, [inputs.distance, inputs.units, inputs.hours, inputs.minutes, inputs.seconds]); |
There was a problem hiding this comment.
liveVdot is calculated for any nonzero time, even when the time fields are in an invalid state (e.g. minutes/seconds >= 60). This can display a VDOT badge that doesn’t correspond to a valid race time. Consider mirroring validatePaceInputs here (return null when minutes/seconds are >= 60 or when errors.time is present).
| const totalSecs = timeToSeconds(inputs.hours, inputs.minutes, inputs.seconds); | |
| if (totalSecs <= 0) return null; | |
| const distMeters = inputs.units === "km" ? dist * 1000 : dist * 1609.34; | |
| const vdot = calculateVdot(distMeters, totalSecs); | |
| if (!isFinite(vdot) || vdot < 10 || vdot > 100) return null; | |
| return Math.round(vdot * 10) / 10; | |
| }, [inputs.distance, inputs.units, inputs.hours, inputs.minutes, inputs.seconds]); | |
| if (errors.time) return null; | |
| const minutes = parseInt(inputs.minutes || "0", 10); | |
| const seconds = parseInt(inputs.seconds || "0", 10); | |
| if (minutes >= 60 || seconds >= 60) return null; | |
| const totalSecs = timeToSeconds(inputs.hours, inputs.minutes, inputs.seconds); | |
| if (totalSecs <= 0) return null; | |
| const distMeters = inputs.units === "km" ? dist * 1000 : dist * 1609.34; | |
| const vdot = calculateVdot(distMeters, totalSecs); | |
| if (!isFinite(vdot) || vdot < 10 || vdot > 100) return null; | |
| return Math.round(vdot * 10) / 10; | |
| }, [inputs.distance, inputs.units, inputs.hours, inputs.minutes, inputs.seconds, errors.time]); |
Replaces the static VDOT badge pill with a <Link to="/vdot"> so users can tap it to open the full VDOT calculator (What-If slider, percentile, training zones, history) without duplicating that functionality in the pace calculator. Adds ArrowRight affordance to signal it's interactive. https://claude.ai/code/session_011ty7rQtakAjWbks15dqSXE

(e.g. "Sub 3h", "3:30", "4:00" for Marathon) that instantly populate
the HH:MM:SS fields and auto-calculate training paces on tap
slider (Marathon 2h–7h, 5K 12min–45min, etc.)
user types or drags the slider – powered by Jack Daniels' formula
a valid time is entered; dragging updates HH/MM/SS fields in real time
contextually shown only for known race distances
https://claude.ai/code/session_011ty7rQtakAjWbks15dqSXE