Mobile Exercise Management Tab¶
Overview¶
The mobile exercise management tab gives therapists full exercise library management directly from the mobile app. Previously, exercise library management was only available on the web (OktaPT-FE/pages/doctor/exercise-library.tsx). The mobile app had a basic exercise search in the personalized-video flow, but no full library browsing, filtering, creating, or editing.
This feature adds a dedicated Exercises tab for doctors with:
- Full exercise library browsing with search, tag filtering (AND/OR mode), and segment control
- Exercise detail view with video playback (Cloudflare Stream, Kinescope, YouTube)
- Create/edit screens with device camera and photo library integration via expo-image-picker
- Favorite toggling with haptic feedback
- Feature-flagged behind therapistExerciseManagement tenant feature flag
Gating: Only visible to doctors whose tenant has therapistExerciseManagement enabled. Uses existing TenantFeaturesContext + hasFeature() pattern. Cross-ref feature-flags.md.
Tab Registration¶
Tab Bar Config¶
File: Okta-Mobile/components/navigation/tabBarConfig.ts
The exercises tab is registered at index 2 (between Messages and Manage):
{
routeName: "exercises",
iconFilled: "fitness",
iconOutline: "fitness-outline",
color: "#06B6D4", // Cyan
colorLight: "rgba(6,182,212,0.15)",
}
| Property | Value |
|---|---|
| Icon | fitness / fitness-outline (Ionicons) |
| Accent Color | Cyan #06B6D4 |
| Pill Tint | rgba(6,182,212,0.15) |
Cross-ref mobile-navigation-design.md for tab bar visual design.
Tab Visibility¶
File: Okta-Mobile/app/(tabs)/_layout.tsx
<Tabs.Screen
name="exercises"
options={{
href: isDoctor && hasFeature("therapistExerciseManagement") ? undefined : null,
title: t("tabTitles.exercises"),
}}
/>
href: nullhides the tab entirely (patients, tenants without the feature)href: undefinedshows the tab (doctors with the feature enabled)- Requires both
useTenantFeatures()anduseAuth()context
Exercise List Screen¶
File: Okta-Mobile/app/(tabs)/exercises.tsx
The main tab screen is a scrollable list with search, filtering, and segmentation.
Layout¶
+------------------------------------------+
| Exercise Library (header, 30px bold) |
+------------------------------------------+
| [Search bar] [Filter badge button] |
| [All] [My Private] [Favorites] |
+------------------------------------------+
| ExerciseCard |
| ExerciseCard |
| ExerciseCard |
| ... |
+------------------------------------------+
| [+ FAB] |
+------------------------------------------+
Features¶
| Feature | Implementation |
|---|---|
| Search | TextInput with debounced client-side filtering via filterExercises() |
| Filter | Badge button showing active filter count, opens TagFilterSheet |
| Segments | 3 toggleable pills: All Exercises, My Private, Favorites |
| List | FlatList with keyboardShouldPersistTaps="handled", keyboardDismissMode="on-drag" |
| Pull-to-refresh | RefreshControl triggers refetch() |
| Create FAB | "+" button positioned above tab bar, navigates to create screen |
| Bottom padding | useTabBarBottomPadding() for floating tab bar clearance |
States¶
| State | Display |
|---|---|
| Loading | LoadingState component (skeleton) |
| Content | FlatList of ExerciseCard items |
| Empty | Guidance message ("No exercises found") |
| Error | ErrorState component with retry button |
Client-Side Filtering¶
File: Okta-Mobile/lib/exerciseFilters.ts
| Dimension | Logic |
|---|---|
| Name | Case-insensitive includes |
| Tags (AND mode) | All selected tags must be present on the exercise |
| Tags (OR mode) | At least one selected tag must be present |
| Segment: All | No filter |
| Segment: My Private | !isPublic && createdById === userId |
| Segment: Favorites | isFavorite === true |
Exercise Card¶
File: Okta-Mobile/components/exercises/ExerciseCard.tsx
Each exercise renders as a card with thumbnail, metadata, tags, and a favorite toggle.
Content¶
| Element | Details |
|---|---|
| Thumbnail | 80x80px rounded, via getExerciseThumbnailUrl(exercise) |
| Play overlay | Shown if hasVideoSource(exercise) is true |
| Name | 16px weight 600, truncated to 1 line |
| Category | 12px, capitalized (categoryId.replace(/_/g, " ")) |
| Creator | 13px muted, "by FirstName LastName" or "System" |
| Tags | Up to 2 TagChip components + "+N more" indicator |
| Favorite star | Pressable, haptic feedback via hapticLight() |
Thumbnail Priority¶
Uses getExerciseThumbnailUrl() from lib/exerciseUtils.ts:
- Personalized video (
personalizedVideo.cloudflareStreamId) - Kinescope (
kinescopeVideoId) →https://kinescope.io/{id}/poster.jpg - Cloudflare Stream (
cloudflareStreamId) →https://customer-j8qsy7zjqvqbtuqp.cloudflarestream.com/{id}/thumbnails/thumbnail.jpg?time=1s - YouTube (extracted from
videoUrl) →https://img.youtube.com/vi/{id}/hqdefault.jpg - Direct photo (
photoUrl) - Placeholder (barbell icon)
Cross-ref exercise-media-rendering.md for full thumbnail/video rendering details.
Press Interaction¶
- Card press: navigates to
/(doctor)/(exercises)/detail?exerciseId=... - Press animation via
createPressHandlers()(scale spring fromlib/designSystem.ts) - Favorite star:
hapticLight()on press
Tag Chip¶
File: Okta-Mobile/components/exercises/TagChip.tsx
Colored pill component used throughout the exercise management screens.
| Prop | Type | Description |
|---|---|---|
name |
string |
Tag display name |
color |
string \| null |
Hex color (fallback: #6B7280) |
type |
TagType |
Determines emoji prefix |
size |
"sm" \| "md" |
Pill size variant |
onRemove |
() => void |
Shows X button when provided |
onPress |
() => void |
Makes chip tappable |
selected |
boolean |
Higher opacity background/border |
Type Emoji Mapping¶
| TagType | Emoji |
|---|---|
| REGION | pin |
| MUSCLE_GROUP | muscle |
| MOVEMENT_PATTERN | runner |
| EQUIPMENT | weight lifter |
| OBJECTIVE | target |
| SPECIALTY | hospital |
| POSITION | yoga |
| DIFFICULTY_CONTEXT | lightning |
| CUSTOM | label |
Color Handling¶
Tag color is nullable in the database (String? in Prisma). The component uses a fallback:
- Default state: background at 15% opacity (
+ "26"), border at 30% opacity (+ "4D") - Selected state: background at 20% opacity (
+ "33"), border at 50% opacity (+ "80")
Exercise List Header¶
File: Okta-Mobile/components/exercises/ExerciseListHeader.tsx
Combined header widget containing search, filter, and segment control.
Search Bar¶
- Left icon (search), center
TextInput, right clear button - Placeholder: "Search exercises..."
- Debounced filtering applied to exercise list
Filter Button¶
- Shows badge with count of selected tags
- Tapping opens
TagFilterSheet
Segment Control¶
- 3 pills: All Exercises | My Private | Favorites
- Styled like the login toggle: gray container, active pill gets brand color background + white text
- Segment definitions use
as constfor properNestedKeyOf<TranslationKeys>type inference
Tag Filter Bottom Sheet¶
File: Okta-Mobile/components/exercises/TagFilterSheet.tsx
Shared modal bottom sheet used for both: - filtering the exercise library by tags - selecting tags while creating/editing an exercise
Layout¶
+------------------------------------------+
| (backdrop - tap to dismiss) |
| |
| +--------------------------------------+ |
| | Filter by Tags [X close] | |
| | | |
| | [Match All] [Match Any] | | <- filter mode only
| | | |
| | [Search tags...] | |
| | | |
| | > Region (2 selected) | |
| | [Tag] [Tag] [Tag] [Tag] | |
| | | |
| | > Movement Pattern | |
| | [Tag] [Tag] [Tag] | |
| | | |
| | [Clear All] [Apply] | |
| +--------------------------------------+ |
+------------------------------------------+
Features¶
| Feature | Implementation |
|---|---|
| Backdrop | rgba(0,0,0,0.5), tap-to-dismiss |
| Container | White bottom sheet, 24px top border radius, fixed height with scrollable body |
| Mode toggle | AND ("Match All") / OR ("Match Any"), shown only for exercise-library filtering |
| Tag search | Filters tag name and key within sections |
| Sections | Grouped by TagType, each collapsible |
| Section headers | Type name + emoji + selected count badge |
| Tag chips | Toggleable TagChip components in a flex wrap grid |
| States | Explicit loading, error + retry, and empty states |
| Bottom bar | "Clear All" (ghost) + "Apply" (primary) |
Default Expanded Sections¶
REGION, MOVEMENT_PATTERN, EQUIPMENT, POSITION — other sections start collapsed.
Group Ordering¶
Tags are rendered in this stable order:
1. REGION
2. MOVEMENT_PATTERN
3. EQUIPMENT
4. POSITION
5. OBJECTIVE
6. SPECIALTY
7. DIFFICULTY_CONTEXT
8. MUSCLE_GROUP
9. CUSTOM
Tags within each section are sorted alphabetically by display name.
Search Behavior¶
- If search is empty, default expanded sections are preserved
- If search is non-empty, all matching sections auto-expand so results are immediately visible
- Used in create/edit screens with
showModeSelector={false}to hide filter-only controls
Hermes Engine Compatibility¶
The component avoids JavaScript features that are unreliable in React Native's Hermes engine:
- Uses TagType[] array instead of Set<TagType> for expandedSections state
- Uses array-safe iteration/grouping helpers
- Uses Array.isArray(tags) ? tags : [] safety guard before iteration
Exercise Detail Screen¶
File: Okta-Mobile/app/(doctor)/(exercises)/detail.tsx
Full-screen exercise detail view, navigated to from ExerciseCard.
Layout¶
- Header: Back button
- Video section: Renders based on available video source
- Cloudflare Stream: WebView iframe (
https://customer-j8qsy7zjqvqbtuqp.cloudflarestream.com/{id}/iframe) - Kinescope: wrapped web-app player via
${EXPO_PUBLIC_WEBVIEW_BASE_URL}/embed/k/{id} - Photo only:
Imagecomponent - Exercise name: 22px bold
- Difficulty badge: Color-coded
- Description: Full text
- Tags section:
TagChipcomponents - Creator info: "by FirstName LastName"
- Action buttons:
- Favorite toggle (star icon)
- Edit (pencil, only if
createdById === userId) → navigate to edit screen - Delete (trash, only if
createdById === userId) → confirmation alert → soft-delete
Data Fetching¶
- Fetches base exercise via
exerciseService.getExercise(id) - Fetches associated tags separately via
exerciseService.getExerciseTags(id, currentLocale) - Merges both responses into local screen state before rendering
- Refetches on screen focus via
useFocusEffect(...), so returning from edit shows updated fields immediately - Media view is keyed off the current media fields so updated video/photo sources refresh immediately after edit
Create/Edit Screens¶
Route Group¶
File: Okta-Mobile/app/(doctor)/(exercises)/_layout.tsx
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="detail" />
<Stack.Screen name="create" />
<Stack.Screen name="edit" />
</Stack>
Registered in Okta-Mobile/app/(doctor)/_layout.tsx as <Stack.Screen name="(exercises)" />.
Exercise Form¶
File: Okta-Mobile/components/exercises/ExerciseForm.tsx
Shared form component for both create and edit modes.
| Field | Type | Required | Notes |
|---|---|---|---|
| Name | TextInput (56px) | Yes | Focused state border highlight |
| Description | TextInput (multiline, 4 rows) | No | Free-text instructions |
| Category | Picker/modal | Yes | Values from ExerciseCategory enum (12 options) |
| Difficulty | Picker/modal | No | Values from DifficultyLevel enum (10 options) |
| Public | Switch toggle | No | Controls exercise visibility |
| Tags | Tag picker | No | Opens TagFilterSheet in selection mode |
| Video | Media picker | No | Record or choose from library |
| Photo | Media picker | No | Take photo or choose from library |
Media Picker¶
File: Okta-Mobile/components/exercises/MediaPicker.tsx
Uses expo-image-picker for system camera UI (like iMessage action sheet experience).
pickVideo(t, onResult) // For video capture/selection
pickPhoto(t, onResult) // For photo capture/selection
| Platform | Action Sheet |
|---|---|
| iOS | ActionSheetIOS.showActionSheetWithOptions() |
| Android | Alert.alert() with button options |
| Option | API Call |
|---|---|
| Record Video | ImagePicker.launchCameraAsync({ mediaTypes: ['videos'] }) |
| Choose from Library | ImagePicker.launchImageLibraryAsync({ mediaTypes: ['videos'] }) |
| Take Photo | ImagePicker.launchCameraAsync({ mediaTypes: ['images'] }) |
| Choose from Library | ImagePicker.launchImageLibraryAsync({ mediaTypes: ['images'] }) |
Permission requests handled via ImagePicker.requestCameraPermissionsAsync() and ImagePicker.requestMediaLibraryPermissionsAsync().
Type workaround: The t function has a strict NestedKeyOf<TranslationKeys> type. MediaPicker functions use TranslateFn = (key: any, params?: Record<string, any>) => string to avoid type conflicts when passing t across module boundaries.
Create Screen¶
File: Okta-Mobile/app/(doctor)/(exercises)/create.tsx
Submit flow:
1. Create exercise via POST /v2/exercises (name, description, categoryId, isPublic, etc.)
2. If video file selected: get upload URL → upload to S3 → confirm with webhook token
3. If photo file selected: get upload URL → upload to S3
4. Save tags via PUT /v2/exercises/exercise/:exerciseId/tags
5. Show SuccessToast, navigate back, trigger refetch
Upload errors are non-blocking — exercise creation still succeeds even if media upload fails.
Edit Screen¶
File: Okta-Mobile/app/(doctor)/(exercises)/edit.tsx
- Receives
exerciseIdparam - Fetches current exercise data via
exerciseService.getExercise(id)plusexerciseService.getExerciseTags(id, currentLocale) - Pre-fills
ExerciseFormfields - Submit:
PATCH /exercises/:id+ upload new media + update tags - Can delete existing media via "Remove" buttons →
DELETE /exercises/:id/media/:type
Types¶
File: Okta-Mobile/types/exercise.ts
Interfaces¶
| Type | Purpose |
|---|---|
Exercise |
Full exercise object with all fields |
Tag |
Tag with id, key, type, color (nullable), name, description |
ExerciseFilters |
Filter state: name, tags, tagMode (all/any), segment (all/myPrivate/favorites) |
CreateExerciseData |
POST body for creating exercises |
UpdateExerciseData |
PATCH body for updating exercises |
Enums¶
| Enum | Values |
|---|---|
ExerciseCategory |
12 values: UPPER_BODY, LOWER_BODY, CORE, CARDIO, STRETCHING, BALANCE, MOBILITY, REHABILITATION, MIDSECTION, FULL_BODY, OTHER, NOT_SPECIFIED |
DifficultyLevel |
10 values: BEGINNER through LEGENDARY, OTHER, NOT_SPECIFIED |
TagType |
9 values: REGION, MUSCLE_GROUP, MOVEMENT_PATTERN, EQUIPMENT, OBJECTIVE, SPECIALTY, POSITION, DIFFICULTY_CONTEXT, CUSTOM |
Lookup Maps¶
TAG_TYPE_EMOJI: Record<TagType, string>— emoji for each tag typeTAG_TYPE_LABELS: Record<TagType, string>— display label for each tag type
Service Layer¶
File: Okta-Mobile/services/exerciseService.ts
Singleton service using httpClient for all exercise API calls.
| Method | HTTP | Endpoint | Notes |
|---|---|---|---|
listExercises(lang) |
GET | /v2/exercises?lang=... |
Unwraps response.exercises |
getExercise(id) |
GET | /v2/exercises/exercise/:id |
Returns full exercise |
createExercise(data) |
POST | /v2/exercises |
Creates new exercise |
updateExercise(id, data) |
PATCH | /exercises/:id |
Updates exercise fields |
deleteExercise(id) |
DELETE | /exercises/:id |
Soft-deletes exercise |
addFavorite(id) |
POST | /exercises/:id/favorite |
Adds to favorites |
removeFavorite(id) |
DELETE | /exercises/:id/favorite |
Removes from favorites |
toggleFavorite(id, isFav) |
— | — | Calls add/remove based on current state |
getMediaUploadUrl(id, type, contentType) |
POST | /exercises/:id/media/upload-url |
Returns signed S3 URL + webhook token |
confirmVideoUpload(token) |
POST | /exercises/upload-complete/:token |
Triggers Cloudflare Stream processing |
deleteMedia(id, type) |
DELETE | /exercises/:id/media/:type |
Deletes video or photo |
getExerciseTags(id, lang) |
GET | /v2/exercises/exercise/:id/tags?lang=... |
Unwraps response.tags |
getTags(lang, includeCustom) |
GET | /v2/exercises/tags?lang=...&includeCustom=... |
Unwraps response.tags |
replaceExerciseTags(id, tagKeys) |
PUT | /v2/exercises/exercise/:id/tags |
Replaces all tags |
Response Unwrapping¶
The API wraps responses in container objects. The service unwraps them:
// exercises API returns { exercises: [...] }
async listExercises(lang: string): Promise<Exercise[]> {
const response = await httpClient.get<ExercisesResponse>(`/v2/exercises?lang=${lang}`);
return response.exercises || [];
}
// tags API returns { tags: [...], language, total }
async getTags(lang: string, includeCustom: boolean = true): Promise<Tag[]> {
const response = await httpClient.get<TagsResponse>(
`/v2/exercises/tags?lang=${lang}&includeCustom=${includeCustom}`
);
return response.tags || [];
}
async getExerciseTags(exerciseId: number, lang: string): Promise<Tag[]> {
const response = await httpClient.get<{ tags: Tag[] }>(
`/v2/exercises/exercise/${exerciseId}/tags?lang=${lang}`
);
return response.tags || [];
}
getExercise(id) returns the raw exercise record from the detail endpoint. Tags are fetched separately via getExerciseTags(...) for detail/edit screens.
Hooks¶
useExercises¶
File: Okta-Mobile/hooks/useExercises.ts
- Fetches all exercises on mount via
exerciseService.listExercises(currentLocale) - Locale sourced from
useTranslation()context - Includes
Array.isArray(data) ? data : []safety guard - Refetches when locale changes
useTags¶
File: Okta-Mobile/hooks/useTags.ts
- Fetches all tags on mount via
exerciseService.getTags(currentLocale, true) - Same locale handling and safety guards as
useExercises
API Changes¶
Exercise List Endpoint¶
File: OktaPT-API/src/routes/exercises/exercises.ts
Added cloudflareStreamId: true and kinescopeVideoId: true to the Prisma select clause in the GET /v2/exercises handler. This allows the mobile app to generate video thumbnails directly from the list response without making additional per-exercise API calls.
Background: The web app doesn't include these fields in the exercise list response. Instead, it uses a separate useExerciseMedia hook that fetches GET /exercises/:id/media for each exercise with an uploadedVideoKey. This N+1 pattern works on web but is inefficient for mobile. Adding the fields directly to the list response is a more efficient approach.
Search Endpoint¶
File: OktaPT-API/src/services/exerciseService.ts
The searchExercises() function (used when a search query q is provided) already includes cloudflareStreamId: true in its select clause.
App Config¶
File: Okta-Mobile/app.config.js
Added NSPhotoLibraryUsageDescription to iOS infoPlist for expo-image-picker photo library access.
Translations¶
Added to both Okta-Mobile/locales/en.json and Okta-Mobile/locales/ru.json:
Tab Title¶
Exercise Library Section¶
All keys under exerciseLibrary.*:
| Key | EN | Purpose |
|---|---|---|
title |
Exercise Library | Screen title |
search |
Search exercises... | Exercise search placeholder |
filterByTags |
Filter by Tags | Filter sheet title |
matchAll |
Match All | AND mode label |
matchAny |
Match Any | OR mode label |
searchTags |
Search tags... | Tag search placeholder |
loadingTags |
Loading tags... | Tag sheet loading state |
noTagsFound |
No tags found | Tag sheet empty state |
clearAll |
Clear All | Clear filters button |
apply |
Apply | Apply button |
allExercises |
All Exercises | Segment label |
myPrivate |
My Private | Segment label |
favorites |
Favorites | Segment label |
noExercises |
No exercises found | Empty state |
noExercisesSubtext |
Try adjusting your search or filters | Empty state hint |
createdBy |
by {{name}} | Creator attribution |
system |
System | System exercise creator |
createExercise |
Create Exercise | Create screen title |
editExercise |
Edit Exercise | Edit screen title |
name |
Name | Form field label |
description |
Description | Form field label |
category |
Category | Form field label |
difficulty |
Difficulty | Form field label |
public |
Public | Form field label |
tags |
Tags | Form field label |
addTags |
Add Tags | Tag picker button |
video |
Video | Media section label |
addVideo |
Add Video | Video picker button |
photo |
Photo | Media section label |
addPhoto |
Add Photo | Photo picker button |
save |
Save | Submit button |
delete |
Delete | Delete button |
deleteConfirm |
Delete this exercise? | Delete alert title |
deleteMessage |
This action cannot be undone. | Delete alert message |
cancel |
Cancel | Cancel button |
recordVideo |
Record Video | Action sheet option |
chooseFromLibrary |
Choose from Library | Action sheet option |
takePhoto |
Take Photo | Action sheet option |
cameraPermission |
Camera permission is required... | Permission alert |
libraryPermission |
Photo library permission is required... | Permission alert |
Dependencies¶
| Package | Purpose | Install |
|---|---|---|
expo-image-picker |
Camera and photo library access for media capture | npx expo install expo-image-picker |
All other dependencies (react-native, @expo/vector-icons, expo-router, etc.) were already present.
Differences from Web Exercise Library¶
| Aspect | Web (OktaPT-FE) |
Mobile (Okta-Mobile) |
|---|---|---|
| Thumbnail data | Fetches cloudflareStreamId per-exercise via GET /exercises/:id/media |
Gets cloudflareStreamId directly from exercise list response |
| Video playback | @cloudflare/stream-react Stream component |
WebView iframe |
| View modes | Grid (VirtuosoGrid) + Table toggle | Single-column FlatList cards |
| Filter UI | Sidebar drawer (TagFilterDrawer) |
Bottom sheet modal (TagFilterSheet) |
| Media capture | File input (<input type="file">) |
expo-image-picker with camera/library action sheet |
| Haptics | None | hapticLight() on interactive elements |
| Feature gate | Not feature-flagged | therapistExerciseManagement feature flag |
| Tag filtering | Advanced (category, difficulty, muscle, creator, media, tags) | Tag-based only (AND/OR mode) |
Known Considerations¶
Hermes Engine Compatibility¶
React Native's Hermes engine has limitations with certain JavaScript features:
Setiteration:useState<Set<T>>and iterating over Sets can throw "iterator method is not callable". Use arrays instead.for...ofon non-array iterables: Can fail silently or throw. Use.forEach()or.map()instead.- Safety guards: Always use
Array.isArray(data) ? data : []before iterating API responses, as the response shape may not match expectations during error conditions.
Nullable Tag Colors¶
Tag color is String? in Prisma (nullable). All components that render tag colors must provide a fallback:
API Response Wrapping¶
The exercise and tag API endpoints wrap their responses:
- GET /v2/exercises returns { exercises: [...] } (not a raw array)
- GET /v2/exercises/tags returns { tags: [...], language, total } (not a raw array)
The service layer unwraps these before returning to hooks.
Related Documentation¶
| Document | Covers |
|---|---|
| exercise-library.md | Exercise library end-to-end (data model, tag system, API endpoints, security, web UI) |
| exercise-media-rendering.md | Video/thumbnail priority fallback across providers |
| exercise-scope-service.md | Visibility filtering (buildExerciseWhere) |
| feature-flags.md | Per-tenant feature flag system |
| mobile-navigation-design.md | Tab bar design, colors, animations |
| mobile-role-separation.md | Doctor/patient route groups and guards |
| personalized-exercise-videos.md | Per-patient exercise video recording |
| mobile-translations.md | Mobile i18n system |
Key Files¶
New Files¶
| File | Purpose |
|---|---|
Okta-Mobile/types/exercise.ts |
Exercise, Tag, Filter type definitions and enums |
Okta-Mobile/services/exerciseService.ts |
Exercise CRUD, media, tags, favorites API service |
Okta-Mobile/hooks/useExercises.ts |
Exercise list data hook |
Okta-Mobile/hooks/useTags.ts |
Tags data hook |
Okta-Mobile/lib/exerciseFilters.ts |
Client-side exercise filtering utility |
Okta-Mobile/app/(tabs)/exercises.tsx |
Exercise library tab screen |
Okta-Mobile/app/(doctor)/(exercises)/_layout.tsx |
Exercise route group layout |
Okta-Mobile/app/(doctor)/(exercises)/detail.tsx |
Exercise detail screen |
Okta-Mobile/app/(doctor)/(exercises)/create.tsx |
Create exercise screen |
Okta-Mobile/app/(doctor)/(exercises)/edit.tsx |
Edit exercise screen |
Okta-Mobile/components/exercises/ExerciseCard.tsx |
Exercise card component |
Okta-Mobile/components/exercises/ExerciseListHeader.tsx |
Search + filter + segments header |
Okta-Mobile/components/exercises/TagFilterSheet.tsx |
Tag filter bottom sheet |
Okta-Mobile/components/exercises/tagFilterUtils.ts |
Tag section ordering, grouping, sorting, and search behavior |
Okta-Mobile/components/exercises/TagChip.tsx |
Tag chip component |
Okta-Mobile/components/exercises/ExerciseForm.tsx |
Create/edit form component |
Okta-Mobile/components/exercises/MediaPicker.tsx |
Camera/library media picker |
Okta-Mobile/__tests__/tagFilterSheet.test.tsx |
Tag sheet/helper regression coverage |
Modified Files¶
| File | Change |
|---|---|
Okta-Mobile/components/navigation/tabBarConfig.ts |
Added exercises tab config (cyan, fitness icon) |
Okta-Mobile/app/(tabs)/_layout.tsx |
Registered exercises tab (doctor-only, feature-flagged) |
Okta-Mobile/app/(doctor)/_layout.tsx |
Registered (exercises) route group |
Okta-Mobile/locales/en.json |
Added tabTitles.exercises and exerciseLibrary.* section |
Okta-Mobile/locales/ru.json |
Added tabTitles.exercises and exerciseLibrary.* section (Russian) |
Okta-Mobile/app.config.js |
Added NSPhotoLibraryUsageDescription for expo-image-picker |
OktaPT-API/src/routes/exercises/exercises.ts |
Added cloudflareStreamId to exercise list select |
Reused Existing Code¶
| File | What's Reused |
|---|---|
Okta-Mobile/lib/exerciseUtils.ts |
getExerciseThumbnailUrl(), hasVideoSource(), ExerciseMedia interface |
Okta-Mobile/lib/httpClient.ts |
All API calls via singleton HTTP client |
Okta-Mobile/lib/designSystem.ts |
hapticLight(), hapticMedium(), hapticSuccess(), createPressHandlers() |
Okta-Mobile/components/navigation/tabBarConfig.ts |
useTabBarBottomPadding() |
Okta-Mobile/components/LoadingState.tsx |
Loading screen component |
Okta-Mobile/components/ErrorState.tsx |
Error screen with retry |
Okta-Mobile/components/SuccessToast.tsx |
Success notifications |
Okta-Mobile/context/TenantFeaturesContext.tsx |
hasFeature() for feature flag gating |
Okta-Mobile/context/I18nContext.tsx |
useTranslation() for i18n |