Skip to content

Commit 42ac67b

Browse files
committed
Adds hole image display in course banner
Enhances the user experience by embedding a visual hole layout below course info Ensures accessibility with alt text and graceful error handling for broken images References new documentation covering future enhancements
1 parent ebfc454 commit 42ac67b

File tree

5 files changed

+307
-1
lines changed

5 files changed

+307
-1
lines changed

.ai-context.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ Must-read before working on these areas:
5959
| Event handling | [ACTIVITY_SESSION_STATE.md](./docs/technical/ACTIVITY_SESSION_STATE.md) |
6060
| Measurement display | [MEASUREMENT_TILES_MULTI_EVENT.md](./docs/technical/MEASUREMENT_TILES_MULTI_EVENT.md) |
6161
| Hole/shot tracking | [SHOT_NUMBER_CARRY_FORWARD_FIX.md](./docs/technical/SHOT_NUMBER_CARRY_FORWARD_FIX.md) |
62+
| Course info & images | [HOLE_IMAGE_DISPLAY.md](./docs/technical/HOLE_IMAGE_DISPLAY.md) |
6263
| Unit conversion | [UNIT_CONVERSION_SYSTEM.md](./docs/technical/UNIT_CONVERSION_SYSTEM.md) |
6364
| Filtering events | [DEVICE_ID_FILTERING.md](./docs/technical/DEVICE_ID_FILTERING.md) |
6465
| Local development | [WEBHOOK_LOCAL_FIX.md](./docs/technical/WEBHOOK_LOCAL_FIX.md) |
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
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

docs/technical/README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
- [Unit Conversion System](./UNIT_CONVERSION_SYSTEM.md) - Imperial/Metric unit conversion
1111
- [Device ID Filtering](./DEVICE_ID_FILTERING.md) - Device and bay filtering system
1212
- [Session Indicators Feature](./SESSION_INDICATORS_FEATURE.md) - Visual session indicators
13+
- [Hole Image Display](./HOLE_IMAGE_DISPLAY.md) - Visual hole layout images in course info banner
1314

1415
### Bug Fixes & Improvements
1516
- [Shot Number Carry-Forward Fix](./SHOT_NUMBER_CARRY_FORWARD_FIX.md) - Making hole/shot persist across events
@@ -207,6 +208,7 @@ const payload = event?.data?.EventModel || event?.EventModel || event;
207208
2. **Check event handling**[MEASUREMENT_TILES_MULTI_EVENT.md](./MEASUREMENT_TILES_MULTI_EVENT.md)
208209
3. **Check filtering**[DEVICE_ID_FILTERING.md](./DEVICE_ID_FILTERING.md)
209210
4. **Check recent fixes**[SHOT_NUMBER_CARRY_FORWARD_FIX.md](./SHOT_NUMBER_CARRY_FORWARD_FIX.md)
211+
5. **Check course display**[HOLE_IMAGE_DISPLAY.md](./HOLE_IMAGE_DISPLAY.md)
210212

211213
### Event Types Reference
212214

@@ -223,8 +225,9 @@ const payload = event?.data?.EventModel || event?.EventModel || event;
223225
**Frontend**:
224226
- `src/components/WebhookInspector.tsx` - Main event display
225227
- `src/components/MeasurementTilesView.tsx` - Measurement tiles
226-
- `src/components/CourseInfoBanner.tsx` - Course info display
228+
- `src/components/CourseInfoBanner.tsx` - Course info display with hole images
227229
- `src/hooks/useActivitySessionState.ts` - Session state hook
230+
- `src/graphql/queries.ts` - GraphQL queries including course/hole data
228231

229232
**Backend**:
230233
- `server/src/index.ts` - Express server, webhook endpoint

src/components/CourseInfoBanner.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,19 @@
107107
opacity: 0.5;
108108
}
109109
}
110+
111+
/* Hole Image Styles */
112+
.hole-image-container {
113+
margin-top: 12px;
114+
padding-top: 12px;
115+
border-top: 1px solid rgba(255, 255, 255, 0.2);
116+
display: flex;
117+
justify-content: center;
118+
align-items: center;
119+
}
120+
121+
.hole-image {
122+
max-width: 100%;
123+
max-height: 300px;
124+
object-fit: contain;
125+
}

src/components/CourseInfoBanner.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,29 @@ const CourseInfoBanner: React.FC<Props> = ({ sessionData, isLoading, eventHole,
6262
)}
6363
</div>
6464
</div>
65+
66+
{/* Display hole image if available */}
67+
{eventHole !== undefined && courseInfo?.holes && (
68+
<div className="hole-image-container">
69+
{(() => {
70+
const hole = courseInfo.holes.find(h => h.holeNumber === eventHole);
71+
if (hole?.images && hole.images.length > 0) {
72+
return (
73+
<img
74+
src={hole.images[0].url}
75+
alt={`Hole ${eventHole} layout`}
76+
className="hole-image"
77+
onError={(e) => {
78+
// Hide image if it fails to load
79+
(e.target as HTMLElement).style.display = 'none';
80+
}}
81+
/>
82+
);
83+
}
84+
return null;
85+
})()}
86+
</div>
87+
)}
6588
</div>
6689
);
6790
};

0 commit comments

Comments
 (0)