Add onboarding flow and per-event crew roles#17
Conversation
📝 WalkthroughWalkthroughThis PR restructures crew role assignment from a per-member profile field to per-event assignments, adding a shared ChangesPer-event crew roles migration
First-launch onboarding flow
Estimated code review effort: 3 (Moderate) | ~30 minutes Sequence Diagram(s)sequenceDiagram
participant CrewContext
participant migrateLegacyCrewRoles
participant AsyncStorage
CrewContext->>AsyncStorage: load crew members and event assignments
CrewContext->>migrateLegacyCrewRoles: pass members and assignments
migrateLegacyCrewRoles->>migrateLegacyCrewRoles: copy legacy role into roles array
migrateLegacyCrewRoles->>migrateLegacyCrewRoles: strip role/customRole from member
migrateLegacyCrewRoles-->>CrewContext: return members, assignments, changed
CrewContext->>AsyncStorage: persist updated data if changed
sequenceDiagram
participant AppNavigator
participant isOnboardingComplete
participant OnboardingScreen
AppNavigator->>isOnboardingComplete: check flag on mount
isOnboardingComplete-->>AppNavigator: return boolean
AppNavigator->>AppNavigator: set initialRouteName Onboarding or Main
OnboardingScreen->>OnboardingScreen: user completes slides
OnboardingScreen->>AppNavigator: reset navigation to Main or Subscription
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (4)
src/context/CrewContext.tsx (1)
106-143: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMigration wiring looks correct.
Concurrent read/migrate/write is race-free enough here (migration is a pure function of the snapshot, so a redundant overlapping call just rewrites the same values). One minor note: the double cast
members as unknown as CrewMember[]at Line 132 bypasses structural typing —StoredCrewMemberRecordonly guaranteesid: string, so a legacy record missingname/phone/etc. would silently produce an ill-typedCrewMember. Not a regression from this PR, but worth tightening if legacy data shapes are a concern.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/context/CrewContext.tsx` around lines 106 - 143, The cast in loadCrewMembers is too permissive and can hide invalid legacy records, so tighten the typing at the CrewContext migration/load path. In loadCrewMembers, validate or map the migrated StoredCrewMemberRecord data into a proper CrewMember shape before calling setCrewMembers, rather than using members as unknown as CrewMember[]. Keep the existing migrateLegacyCrewRoles flow, but ensure only records with the required CrewMember fields are accepted or normalized before sorting and storing.src/lib/eventCrew.ts (1)
8-8: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueType-only circular import between
eventCrew.tsandCrewContext.tsx.
eventCrew.tsimportsCrewRole(type-only) from../context/CrewContext, whileCrewContext.tsximportsEVENT_CREW_KEY/migrateLegacyCrewRoles/etc. from../lib/eventCrew. This is a circular module reference.import typeis normally erased at compile time so this shouldn't break at runtime, but it's fragile — a future edit that turnsCrewRoleinto a value import (e.g., adding aROLE_CONFIGre-export) would introduce a real circular dependency.Consider moving
CrewRole(and other shared crew domain types) into a small dedicated types module (e.g.src/types/crew.ts) that both files import from.Also applies to: 10-10
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/lib/eventCrew.ts` at line 8, The import in eventCrew.ts creates a fragile circular dependency with CrewContext.tsx because CrewRole is being pulled from the context module even though both files share crew domain types. Move CrewRole and any other shared crew types out of CrewContext.tsx into a dedicated shared types module, then update eventCrew.ts and CrewContext.tsx to import from that module instead of referencing each other directly. Keep the existing eventCrew symbols like EVENT_CREW_KEY and migrateLegacyCrewRoles in place, but ensure CrewContext.tsx no longer serves as the source for shared type definitions.src/screens/events/EventDetailScreen.tsx (1)
507-511: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueInconsistent empty-roles fallback vs. CrewDetailScreen.
When
roles.length === 0, this shows the member's phone number as secondary text, whereasCrewDetailScreen's equivalent section always shows "No roles set" regardless of phone. Consider using the same "No roles set" fallback here for consistency.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/screens/events/EventDetailScreen.tsx` around lines 507 - 511, The empty-roles fallback in EventDetailScreen is inconsistent with CrewDetailScreen because the ternary in the member details rendering uses member.phone when roles.length is 0. Update the EventDetailScreen section that renders the member secondary text so it always shows "No roles set" for the no-roles case, matching CrewDetailScreen’s behavior and keeping the fallback consistent.src/components/ExportRacePlanButton.tsx (1)
21-24: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider reusing
loadEventCrewAssignments()instead of the genericreadJson+EVENT_CREW_KEY.This bypasses the shared
eventCrew.tsload helper in favor of the genericreadJson<T>()utility. Functionally equivalent today, but centralizing onloadEventCrewAssignments()avoids two parallel implementations of the same read logic drifting apart later.Also applies to: 36-51, 88-91
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/ExportRacePlanButton.tsx` around lines 21 - 24, The import and read path in ExportRacePlanButton should use the shared eventCrew loader instead of duplicating JSON access with EVENT_CREW_KEY and readJson<T>(). Update the component to rely on loadEventCrewAssignments() from eventCrew.ts, and adjust any related mapping/loading logic in the affected sections so the export flow reads assignments through that single helper. This keeps ExportRacePlanButton aligned with the centralized loading behavior and removes the parallel implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/lib/eventCrew.ts`:
- Around line 12-20: The event-crew save flow can still persist duplicate rows
because `handleSave` appends `newAssignments` to `allCrew` and
`saveEventCrewAssignments` writes the array unchanged even when the same crew
member is re-selected. Update the `EventCrewAssignment` handling in
`eventCrew.ts` so `handleSave` or `saveEventCrewAssignments` deduplicates by the
`(eventId, crewMemberId)` pair before persisting, and ensure the selection logic
tied to `alreadyAddedIds` in `availableCrew` cannot re-add an existing
assignment once current rows are loaded.
In `@src/screens/crew/CrewDetailScreen.tsx`:
- Around line 245-309: The Assigned Events list in CrewDetailScreen still
renders orphaned EventCrewAssignment records as “Unknown event” after an event
is deleted. Update the event-deletion flow to also remove all related
assignments (or cascade-delete them at the storage layer) so CrewDetailScreen’s
getEvent/assignments.map path only sees valid events and no stale rows remain.
In `@src/screens/events/EventDetailScreen.tsx`:
- Around line 44-52: Event crew persistence is still being handled manually in
EventDetailScreen instead of using the shared helpers from eventCrew. Replace
the direct AsyncStorage getItem/setItem plus JSON.parse/stringify logic with
loadEventCrewAssignments() and saveEventCrewAssignments() from
../../lib/eventCrew, and keep the existing EVENT_CREW_KEY/EventCrewAssignment
references aligned with that module. Update the crew-loading and crew-saving
paths in EventDetailScreen so all event-crew reads and writes go through the
centralized helper functions and no duplicate persistence logic remains.
---
Nitpick comments:
In `@src/components/ExportRacePlanButton.tsx`:
- Around line 21-24: The import and read path in ExportRacePlanButton should use
the shared eventCrew loader instead of duplicating JSON access with
EVENT_CREW_KEY and readJson<T>(). Update the component to rely on
loadEventCrewAssignments() from eventCrew.ts, and adjust any related
mapping/loading logic in the affected sections so the export flow reads
assignments through that single helper. This keeps ExportRacePlanButton aligned
with the centralized loading behavior and removes the parallel implementation.
In `@src/context/CrewContext.tsx`:
- Around line 106-143: The cast in loadCrewMembers is too permissive and can
hide invalid legacy records, so tighten the typing at the CrewContext
migration/load path. In loadCrewMembers, validate or map the migrated
StoredCrewMemberRecord data into a proper CrewMember shape before calling
setCrewMembers, rather than using members as unknown as CrewMember[]. Keep the
existing migrateLegacyCrewRoles flow, but ensure only records with the required
CrewMember fields are accepted or normalized before sorting and storing.
In `@src/lib/eventCrew.ts`:
- Line 8: The import in eventCrew.ts creates a fragile circular dependency with
CrewContext.tsx because CrewRole is being pulled from the context module even
though both files share crew domain types. Move CrewRole and any other shared
crew types out of CrewContext.tsx into a dedicated shared types module, then
update eventCrew.ts and CrewContext.tsx to import from that module instead of
referencing each other directly. Keep the existing eventCrew symbols like
EVENT_CREW_KEY and migrateLegacyCrewRoles in place, but ensure CrewContext.tsx
no longer serves as the source for shared type definitions.
In `@src/screens/events/EventDetailScreen.tsx`:
- Around line 507-511: The empty-roles fallback in EventDetailScreen is
inconsistent with CrewDetailScreen because the ternary in the member details
rendering uses member.phone when roles.length is 0. Update the EventDetailScreen
section that renders the member secondary text so it always shows "No roles set"
for the no-roles case, matching CrewDetailScreen’s behavior and keeping the
fallback consistent.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: c917673c-631a-4e1d-84b6-0f3af6dd54e9
📒 Files selected for processing (15)
jest.setup.jssrc/__tests__/crewMigration.test.tssrc/__tests__/onboarding.test.tssrc/components/ExportRacePlanButton.tsxsrc/context/CrewContext.tsxsrc/lib/eventCrew.tssrc/lib/onboarding.tssrc/navigation/AppNavigator.tsxsrc/screens/crew/CreateCrewScreen.tsxsrc/screens/crew/CrewDetailScreen.tsxsrc/screens/crew/CrewListScreen.tsxsrc/screens/crew/EditCrewScreen.tsxsrc/screens/events/EventDetailScreen.tsxsrc/screens/events/SelectCrewScreen.tsxsrc/screens/onboarding/OnboardingScreen.tsx
| export interface EventCrewAssignment { | ||
| eventId: string; | ||
| crewMemberId: string; | ||
| /** A member can hold multiple roles for a single event. */ | ||
| roles: CrewRole[]; | ||
| /** Custom label used when `roles` includes 'other'. */ | ||
| customRole?: string | null; | ||
| notes?: string; | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Inspect how SelectCrewScreen initializes selectedIds and whether it excludes existing assignments
rg -n -B3 -A40 'const handleSave' src/screens/events/SelectCrewScreen.tsx
rg -n -B5 -A15 'selectedIds' src/screens/events/SelectCrewScreen.tsx | head -80Repository: beaux-riel/UltraEdge
Length of output: 4363
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== SelectCrewScreen.tsx relevant section ==\n'
sed -n '45,125p' src/screens/events/SelectCrewScreen.tsx
printf '\n== saveEventCrewAssignments definition(s) ==\n'
rg -n -A80 -B20 'function saveEventCrewAssignments|const saveEventCrewAssignments|saveEventCrewAssignments\(' srcRepository: beaux-riel/UltraEdge
Length of output: 15697
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== List/render section ==\n'
sed -n '125,310p' src/screens/events/SelectCrewScreen.tsx
printf '\n== crewMembers hook definition ==\n'
rg -n -A80 -B20 'function useCrewMembers|const useCrewMembers|export .*useCrewMembers' srcRepository: beaux-riel/UltraEdge
Length of output: 8095
Prevent duplicate event-crew rows
availableCrew filters out alreadyAddedIds, but handleSave still appends [...allCrew, ...newAssignments] and saveEventCrewAssignments writes the array as-is. Because alreadyAddedIds starts empty and there’s no save-time dedupe, re-selecting an already assigned crew member can still persist a duplicate row. Dedup by (eventId, crewMemberId) or block selection until existing assignments load.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lib/eventCrew.ts` around lines 12 - 20, The event-crew save flow can
still persist duplicate rows because `handleSave` appends `newAssignments` to
`allCrew` and `saveEventCrewAssignments` writes the array unchanged even when
the same crew member is re-selected. Update the `EventCrewAssignment` handling
in `eventCrew.ts` so `handleSave` or `saveEventCrewAssignments` deduplicates by
the `(eventId, crewMemberId)` pair before persisting, and ensure the selection
logic tied to `alreadyAddedIds` in `availableCrew` cannot re-add an existing
assignment once current rows are loaded.
| {/* Assigned Events with per-event roles */} | ||
| <View style={styles.eventsSection}> | ||
| <H3 style={{ marginBottom: spacing.md }}>Assigned Events</H3> | ||
| <Card variant="standard"> | ||
| <CardContent> | ||
| <View style={styles.placeholderContent}> | ||
| <Ionicons name="calendar-outline" size={32} color={colors.mist} /> | ||
| <Body color="secondary" style={{ marginTop: spacing.sm, textAlign: 'center' }}> | ||
| Event assignments coming soon! | ||
| </Body> | ||
| <BodySmall color="tertiary" style={{ marginTop: spacing.xs, textAlign: 'center' }}> | ||
| You'll be able to assign crew members to specific events and checkpoints. | ||
| </BodySmall> | ||
| </View> | ||
| {assignments.length === 0 ? ( | ||
| <View style={styles.placeholderContent}> | ||
| <Ionicons name="calendar-outline" size={32} color={colors.mist} /> | ||
| <Body color="secondary" style={{ marginTop: spacing.sm, textAlign: 'center' }}> | ||
| Not assigned to any events yet. | ||
| </Body> | ||
| <BodySmall color="tertiary" style={{ marginTop: spacing.xs, textAlign: 'center' }}> | ||
| Assign {member.name.split(' ')[0]} to an event to set their roles for race day. | ||
| </BodySmall> | ||
| </View> | ||
| ) : ( | ||
| assignments.map((assignment, index) => { | ||
| const event = getEvent(assignment.eventId); | ||
| const roles = assignment.roles ?? []; | ||
|
|
||
| return ( | ||
| <View | ||
| key={`${assignment.eventId}-${assignment.crewMemberId}`} | ||
| style={[ | ||
| styles.eventRow, | ||
| { | ||
| borderBottomColor: colors.borderLight, | ||
| borderBottomWidth: index < assignments.length - 1 ? 1 : 0, | ||
| }, | ||
| ]} | ||
| > | ||
| <Body numberOfLines={1} style={{ fontWeight: '600' }}> | ||
| {event?.name ?? 'Unknown event'} | ||
| </Body> | ||
| <View style={styles.roleChips}> | ||
| {roles.length === 0 ? ( | ||
| <BodySmall color="tertiary">No roles set</BodySmall> | ||
| ) : ( | ||
| roles.map(role => { | ||
| const config = ROLE_CONFIG[role]; | ||
| const label = | ||
| role === 'other' && assignment.customRole | ||
| ? assignment.customRole | ||
| : config.label; | ||
| return ( | ||
| <View | ||
| key={role} | ||
| style={[styles.roleChip, { backgroundColor: config.color + '20' }]} | ||
| > | ||
| <Ionicons name={config.icon as any} size={12} color={config.color} /> | ||
| <Text | ||
| variant="bodySmall" | ||
| style={{ color: config.color, marginLeft: 4 }} | ||
| > | ||
| {label} | ||
| </Text> | ||
| </View> | ||
| ); | ||
| }) | ||
| )} | ||
| </View> | ||
| </View> | ||
| ); | ||
| }) | ||
| )} |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check whether deleteEvent (or any event-deletion path) cleans up EventCrewAssignment / EVENT_CREW_KEY entries
rg -n -A15 'deleteEvent' src/context/EventContext.tsx
rg -n 'EVENT_CREW_KEY|EventCrewAssignment' src/context/EventContext.tsx src/lib/eventCrew.tsRepository: beaux-riel/UltraEdge
Length of output: 2587
Delete crew assignments when an event is removed. This leaves orphaned EventCrewAssignment rows behind, so members keep seeing Unknown event entries here and the backing storage never gets cleaned up.
[medium_effort_and_high_reward]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/screens/crew/CrewDetailScreen.tsx` around lines 245 - 309, The Assigned
Events list in CrewDetailScreen still renders orphaned EventCrewAssignment
records as “Unknown event” after an event is deleted. Update the event-deletion
flow to also remove all related assignments (or cascade-delete them at the
storage layer) so CrewDetailScreen’s getEvent/assignments.map path only sees
valid events and no stale rows remain.
| import { EVENT_CREW_KEY, EventCrewAssignment } from '../../lib/eventCrew'; | ||
| import { eventStatsFromRoute } from '../../lib/gpx'; | ||
| import GPXRouteSection from '../../components/gpx/GPXRouteSection'; | ||
| import ExportRacePlanButton from '../../components/ExportRacePlanButton'; | ||
|
|
||
| type Props = NativeStackScreenProps<any, 'EventDetail'>; | ||
|
|
||
| // Storage keys for event relationships | ||
| // Storage key for event gear relationships | ||
| const EVENT_GEAR_KEY = '@ultraedge/event-gear'; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Duplicated AsyncStorage access instead of the new shared eventCrew helpers.
This file imports EVENT_CREW_KEY/EventCrewAssignment from ../../lib/eventCrew but still manually calls AsyncStorage.getItem/setItem + JSON.parse/stringify for crew data (Lines 87-106, 189-201) instead of using loadEventCrewAssignments()/saveEventCrewAssignments() from the same module. This duplicates logic the migration introduced specifically to centralize event-crew persistence, and risks the two code paths silently diverging.
♻️ Proposed refactor using the shared helpers
-import { EVENT_CREW_KEY, EventCrewAssignment } from '../../lib/eventCrew';
+import {
+ EventCrewAssignment,
+ loadEventCrewAssignments,
+ saveEventCrewAssignments,
+} from '../../lib/eventCrew'; const loadRelationships = useCallback(async () => {
try {
- const [gearData, crewData] = await Promise.all([
- AsyncStorage.getItem(EVENT_GEAR_KEY),
- AsyncStorage.getItem(EVENT_CREW_KEY),
- ]);
-
- if (gearData) {
- const allGear: EventGearAllocation[] = JSON.parse(gearData);
- setEventGear(allGear.filter(g => g.eventId === eventId));
- }
-
- if (crewData) {
- const allCrew: EventCrewAssignment[] = JSON.parse(crewData);
- setEventCrew(allCrew.filter(c => c.eventId === eventId));
- }
+ const gearData = await AsyncStorage.getItem(EVENT_GEAR_KEY);
+ if (gearData) {
+ const allGear: EventGearAllocation[] = JSON.parse(gearData);
+ setEventGear(allGear.filter(g => g.eventId === eventId));
+ }
+ const allCrew = await loadEventCrewAssignments();
+ setEventCrew(allCrew.filter(c => c.eventId === eventId));
} catch (error) {
console.error('Failed to load event relationships:', error);
}
}, [eventId]); onPress: async () => {
try {
- const stored = await AsyncStorage.getItem(EVENT_CREW_KEY);
- const allCrew: EventCrewAssignment[] = stored ? JSON.parse(stored) : [];
+ const allCrew = await loadEventCrewAssignments();
const updated = allCrew.filter(c => !(c.eventId === eventId && c.crewMemberId === crewMemberId));
- await AsyncStorage.setItem(EVENT_CREW_KEY, JSON.stringify(updated));
+ await saveEventCrewAssignments(updated);
setEventCrew(updated.filter(c => c.eventId === eventId));Also applies to: 87-106, 177-201
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/screens/events/EventDetailScreen.tsx` around lines 44 - 52, Event crew
persistence is still being handled manually in EventDetailScreen instead of
using the shared helpers from eventCrew. Replace the direct AsyncStorage
getItem/setItem plus JSON.parse/stringify logic with loadEventCrewAssignments()
and saveEventCrewAssignments() from ../../lib/eventCrew, and keep the existing
EVENT_CREW_KEY/EventCrewAssignment references aligned with that module. Update
the crew-loading and crew-saving paths in EventDetailScreen so all event-crew
reads and writes go through the centralized helper functions and no duplicate
persistence logic remains.
Summary
src/screens/onboarding/OnboardingScreen.tsx) — plan your race, dial in your gear, rally your crew, and a premium CTA slide. "Start Premium" persists the completion flag before resetting navigation into the Subscription screen; Skip / "Maybe later" go straight into the app. Gate lives inAppNavigatorbehind the@ultraedge/onboarding-completeAsyncStorage flag, rendering a blank parchment view while the flag loads so onboarding never flashes for returning users.role/customRole— roles now live onEventCrewAssignment.roles: CrewRole[](shared type + storage helpers insrc/lib/eventCrew.ts). SelectCrewScreen gets multi-select role chips per member (with a custom label input when "Other" is chosen), so one person can be pacer + driver for one race and medic for another. CrewList/CrewDetail drop the global role badge; CrewDetail now lists assigned events with role chips; EventDetail and the race-plan PDF export render per-event roles.migrateLegacyCrewRoles) copies each member's legacy profile role onto all of their existing event assignments (single-element array, custom label preserved for "other"), then strips the legacy fields from stored member records. Covered by unit tests.Test plan
npx tsc --noEmitpassesnpx jest— 6 suites, 54 passed (includes newcrewMigration.test.tsandonboarding.test.ts)Follow-up note (Supabase)
The app is local-first and crew tables aren't wired to the API yet, so no schema changes were made here. When cloud sync for crew lands,
supabase/migrations/007_crew.sqlneeds rework:event_crew.roleis a single enum and must become an array or a junction table to match the new multi-role-per-assignment model (andcrew_members.role/custom_rolemove off the profile).🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes