|
| 1 | +# Hole Image Display Feature |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +Added visual hole layout image to the CourseInfoBanner component. When viewing events from a course play activity, the banner now displays the hole layout image below the course information and progress section. |
| 6 | + |
| 7 | +## Implementation |
| 8 | + |
| 9 | +### Data Source |
| 10 | + |
| 11 | +Course information is fetched via GraphQL using the `GET_COURSE_INFORMATION` query which includes: |
| 12 | + |
| 13 | +```graphql |
| 14 | +query GetCourseInformation($courseId: ID!) { |
| 15 | + node(id: $courseId) { |
| 16 | + ... on Course { |
| 17 | + displayName |
| 18 | + difficulty |
| 19 | + description |
| 20 | + holes { |
| 21 | + holeNumber |
| 22 | + images { |
| 23 | + url |
| 24 | + metaDataUrl |
| 25 | + } |
| 26 | + } |
| 27 | + } |
| 28 | + } |
| 29 | +} |
| 30 | +``` |
| 31 | + |
| 32 | +Each hole can have multiple images with: |
| 33 | +- `url` - Direct image URL |
| 34 | +- `metaDataUrl` - Optional metadata URL |
| 35 | + |
| 36 | +### Component Changes |
| 37 | + |
| 38 | +#### CourseInfoBanner.tsx |
| 39 | + |
| 40 | +Added a new section at the bottom of the banner: |
| 41 | + |
| 42 | +```tsx |
| 43 | +{/* Display hole image if available */} |
| 44 | +{eventHole !== undefined && courseInfo?.holes && ( |
| 45 | + <div className="hole-image-container"> |
| 46 | + {(() => { |
| 47 | + // Find the hole matching the current event's hole number |
| 48 | + const hole = courseInfo.holes.find(h => h.holeNumber === eventHole); |
| 49 | + |
| 50 | + if (hole?.images && hole.images.length > 0) { |
| 51 | + return ( |
| 52 | + <img |
| 53 | + src={hole.images[0].url} |
| 54 | + alt={`Hole ${eventHole} layout`} |
| 55 | + className="hole-image" |
| 56 | + onError={(e) => { |
| 57 | + // Hide image if it fails to load |
| 58 | + (e.target as HTMLElement).style.display = 'none'; |
| 59 | + }} |
| 60 | + /> |
| 61 | + ); |
| 62 | + } |
| 63 | + return null; |
| 64 | + })()} |
| 65 | + </div> |
| 66 | +)} |
| 67 | +``` |
| 68 | + |
| 69 | +**Key Features**: |
| 70 | +1. **Conditional Display**: Only shows when `eventHole` is available and course has hole data |
| 71 | +2. **Hole Matching**: Finds the hole by matching `holeNumber` with `eventHole` |
| 72 | +3. **First Image**: Uses the first image in the `images` array |
| 73 | +4. **Error Handling**: Hides image if URL fails to load (broken link, CORS, etc.) |
| 74 | +5. **Accessibility**: Includes descriptive `alt` text |
| 75 | + |
| 76 | +### Styling |
| 77 | + |
| 78 | +#### CourseInfoBanner.css |
| 79 | + |
| 80 | +Added styles for the image container and image: |
| 81 | + |
| 82 | +```css |
| 83 | +/* Hole Image Styles */ |
| 84 | +.hole-image-container { |
| 85 | + margin-top: 12px; |
| 86 | + padding-top: 12px; |
| 87 | + border-top: 1px solid rgba(255, 255, 255, 0.2); |
| 88 | + display: flex; |
| 89 | + justify-content: center; |
| 90 | + align-items: center; |
| 91 | +} |
| 92 | + |
| 93 | +.hole-image { |
| 94 | + max-width: 100%; |
| 95 | + max-height: 300px; |
| 96 | + border-radius: 6px; |
| 97 | + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); |
| 98 | + object-fit: contain; |
| 99 | + background: white; |
| 100 | + padding: 4px; |
| 101 | +} |
| 102 | +``` |
| 103 | + |
| 104 | +**Design Choices**: |
| 105 | +- **Separator**: Subtle top border separates image from text content |
| 106 | +- **Centering**: Image is centered horizontally |
| 107 | +- **Max Height**: 300px prevents overly large images |
| 108 | +- **Responsive**: `max-width: 100%` ensures it fits on smaller screens |
| 109 | +- **Object Fit**: `contain` preserves aspect ratio without cropping |
| 110 | +- **White Background**: Provides clean frame for the image |
| 111 | +- **Shadow**: Adds depth and visual separation from banner background |
| 112 | + |
| 113 | +## Visual Layout |
| 114 | + |
| 115 | +``` |
| 116 | +┌─────────────────────────────────────────────────────┐ |
| 117 | +│ 🏌️ Adare Manor │ |
| 118 | +│ Difficulty: 4 │ |
| 119 | +│ The Golf Course at Adare Manor... │ |
| 120 | +│ 18 holes │ |
| 121 | +│ ─────────────────────────────────────── │ |
| 122 | +│ Hole 1 • Shot 1 • Player 1 │ |
| 123 | +│ ─────────────────────────────────────── │ |
| 124 | +│ [ Hole 1 Layout Image Display ] │ |
| 125 | +│ │ |
| 126 | +└─────────────────────────────────────────────────────┘ |
| 127 | +``` |
| 128 | + |
| 129 | +## Data Flow |
| 130 | + |
| 131 | +1. **Session Info Event** received |
| 132 | + - `useActivitySessionState` detects Course activity |
| 133 | + - Calls `FIND_COURSE_ID` GraphQL query |
| 134 | + - Calls `GET_COURSE_INFORMATION` with course ID |
| 135 | + - Stores course data including holes in session state |
| 136 | + |
| 137 | +2. **ChangePlayer Event** received |
| 138 | + - Sets `eventHole` to `ActiveHole` from event |
| 139 | + - Carried forward to subsequent events |
| 140 | + |
| 141 | +3. **Banner Render** |
| 142 | + - Gets `courseInfo` from session state |
| 143 | + - Gets `eventHole` from event or carry-forward |
| 144 | + - Finds matching hole in `courseInfo.holes` |
| 145 | + - Displays first image if available |
| 146 | + |
| 147 | +## Error Handling |
| 148 | + |
| 149 | +### Missing Data Scenarios |
| 150 | + |
| 151 | +1. **No event hole**: Image section doesn't render |
| 152 | +2. **No course info**: Image section doesn't render |
| 153 | +3. **No holes data**: Image section doesn't render |
| 154 | +4. **Hole not found**: Returns `null`, no error thrown |
| 155 | +5. **No images for hole**: Returns `null`, graceful degradation |
| 156 | +6. **Image load failure**: `onError` hides the image element |
| 157 | + |
| 158 | +### Network Issues |
| 159 | + |
| 160 | +- **Broken URL**: Image disappears via `onError` handler |
| 161 | +- **CORS errors**: Image disappears via `onError` handler |
| 162 | +- **Slow loading**: Browser handles loading state |
| 163 | +- **No visual error**: Prevents broken image icon from showing |
| 164 | + |
| 165 | +## Testing |
| 166 | + |
| 167 | +To test the feature: |
| 168 | + |
| 169 | +1. **Prerequisites**: |
| 170 | + - Course play activity with valid course data |
| 171 | + - Course must have hole images configured |
| 172 | + - ActivitySession events must include ChangePlayer |
| 173 | + |
| 174 | +2. **Test Steps**: |
| 175 | + ``` |
| 176 | + 1. Start course play activity on simulator |
| 177 | + 2. View webhook events in inspector |
| 178 | + 3. Click on any event from the session |
| 179 | + 4. Verify banner shows: |
| 180 | + - Course name and info |
| 181 | + - Current hole and shot number |
| 182 | + - Hole layout image at bottom |
| 183 | + ``` |
| 184 | + |
| 185 | +3. **Expected Results**: |
| 186 | + - Image appears below progress section |
| 187 | + - Image is centered and properly sized |
| 188 | + - Image has white frame and shadow |
| 189 | + - Different holes show different images |
| 190 | + |
| 191 | +4. **Edge Cases to Test**: |
| 192 | + - Events before first ChangePlayer (no hole image) |
| 193 | + - Course with missing hole images (graceful degradation) |
| 194 | + - Different holes (image changes correctly) |
| 195 | + - Browser resize (image remains responsive) |
| 196 | + |
| 197 | +## Future Enhancements |
| 198 | + |
| 199 | +Potential improvements: |
| 200 | + |
| 201 | +1. **Multiple Images**: Show image carousel if hole has multiple images |
| 202 | +2. **Metadata**: Display additional info from `metaDataUrl` |
| 203 | +3. **Zoom**: Click image to view full-size |
| 204 | +4. **Loading State**: Show skeleton/spinner while image loads |
| 205 | +5. **Caching**: Cache images to improve performance |
| 206 | +6. **Lazy Loading**: Only load image when banner is visible |
| 207 | +7. **Image Selection**: Let user choose which image to display (overhead, green view, etc.) |
| 208 | + |
| 209 | +## Files Modified |
| 210 | + |
| 211 | +### Frontend Components |
| 212 | +- ✅ `src/components/CourseInfoBanner.tsx` - Added hole image display logic |
| 213 | +- ✅ `src/components/CourseInfoBanner.css` - Added image styling |
| 214 | + |
| 215 | +### GraphQL Queries |
| 216 | +- ℹ️ `src/graphql/queries.ts` - No changes (already includes hole images in `GET_COURSE_INFORMATION`) |
| 217 | + |
| 218 | +### State Management |
| 219 | +- ℹ️ `src/hooks/useActivitySessionState.ts` - No changes (already fetches and stores course info) |
| 220 | + |
| 221 | +## Related Documentation |
| 222 | + |
| 223 | +- [Activity Session State](./ACTIVITY_SESSION_STATE.md) - How course info is fetched and stored |
| 224 | +- [Shot Number Carry-Forward Fix](./SHOT_NUMBER_CARRY_FORWARD_FIX.md) - How hole number is determined |
| 225 | +- GraphQL schema - Course and Hole type definitions |
| 226 | + |
| 227 | +## Performance Considerations |
| 228 | + |
| 229 | +### Image Loading |
| 230 | +- Images are loaded on-demand when banner renders |
| 231 | +- Browser handles caching automatically |
| 232 | +- No lazy loading implemented (images load immediately) |
| 233 | + |
| 234 | +### Memory |
| 235 | +- Images are not stored in state |
| 236 | +- React re-renders are minimal (only when event changes) |
| 237 | +- Error handling prevents memory leaks from failed loads |
| 238 | + |
| 239 | +### Network |
| 240 | +- One image request per hole per session |
| 241 | +- Images may be large (course layouts) |
| 242 | +- Consider implementing lazy loading for large galleries |
| 243 | + |
| 244 | +## Accessibility |
| 245 | + |
| 246 | +- ✅ **Alt Text**: Descriptive alt text includes hole number |
| 247 | +- ✅ **Semantic HTML**: Uses standard `<img>` element |
| 248 | +- ✅ **Responsive**: Works on all screen sizes |
| 249 | +- ⚠️ **Screen Readers**: Consider adding more context |
| 250 | +- ⚠️ **Keyboard Navigation**: Image is not interactive (good for display) |
| 251 | + |
| 252 | +## Browser Compatibility |
| 253 | + |
| 254 | +- ✅ Modern browsers (Chrome, Firefox, Safari, Edge) |
| 255 | +- ✅ CSS features (flexbox, border-radius, box-shadow) |
| 256 | +- ✅ Error handling via `onError` event |
| 257 | +- ✅ Object-fit support (IE11 may need polyfill) |
| 258 | + |
| 259 | +--- |
| 260 | + |
| 261 | +**Added**: October 3, 2025 |
| 262 | +**Purpose**: Enhance visual context for course play events |
| 263 | +**Impact**: Better user understanding of current hole layout |
0 commit comments