@@ -8,8 +8,12 @@ import { format } from "date-fns";
88import {
99 Activity ,
1010 AlertCircle ,
11+ Camera ,
1112 CheckCircle2 ,
13+ ChevronDown ,
14+ ChevronUp ,
1215 Clock ,
16+ Code ,
1317 Loader2 ,
1418 Map ,
1519 RefreshCw ,
@@ -51,6 +55,13 @@ interface StravaActivity {
5155 elapsed_time : number ;
5256 moving_time : number ;
5357 distance ?: number ;
58+ total_photo_count ?: number ;
59+ photos ?: {
60+ primary ?: {
61+ urls ?: Record < string , string > ;
62+ } ;
63+ count ?: number ;
64+ } ;
5465}
5566
5667interface ScoringPreview {
@@ -77,6 +88,23 @@ interface StravaPreviewClientProps {
7788 participantsWithStrava : ParticipantWithStrava [ ] ;
7889}
7990
91+ /**
92+ * Extract the best available photo URL from a Strava activity's photos field.
93+ * Prefers larger resolutions (higher numeric key = higher resolution).
94+ */
95+ function getStravaPhotoUrl (
96+ stravaActivity : StravaActivity
97+ ) : string | null {
98+ const urls = stravaActivity . photos ?. primary ?. urls ;
99+ if ( ! urls ) return null ;
100+
101+ // Keys are resolution strings like "100", "600" — pick the largest
102+ const sortedKeys = Object . keys ( urls ) . sort (
103+ ( a , b ) => Number ( b ) - Number ( a )
104+ ) ;
105+ return sortedKeys . length > 0 ? urls [ sortedKeys [ 0 ] ] : null ;
106+ }
107+
80108export function StravaPreviewClient ( {
81109 challengeId,
82110 participantsWithStrava,
@@ -86,9 +114,22 @@ export function StravaPreviewClient({
86114 const [ error , setError ] = useState < string | null > ( null ) ;
87115 const [ activities , setActivities ] = useState < ActivityWithScoring [ ] | null > ( null ) ;
88116 const [ tokenRefreshed , setTokenRefreshed ] = useState ( false ) ;
117+ const [ expandedJsonIds , setExpandedJsonIds ] = useState < Set < number > > ( new Set ( ) ) ;
89118
90119 const fetchActivities = useAction ( api . actions . strava . fetchActivitiesWithScoringPreview ) ;
91120
121+ const toggleJsonExpanded = ( activityId : number ) => {
122+ setExpandedJsonIds ( ( prev ) => {
123+ const next = new Set ( prev ) ;
124+ if ( next . has ( activityId ) ) {
125+ next . delete ( activityId ) ;
126+ } else {
127+ next . add ( activityId ) ;
128+ }
129+ return next ;
130+ } ) ;
131+ } ;
132+
92133 const selectedParticipant = participantsWithStrava . find ( ( p ) => p . id === selectedUserId ) ;
93134
94135 const handleFetchActivities = async ( ) => {
@@ -249,6 +290,44 @@ export function StravaPreviewClient({
249290 ) }
250291 </ div >
251292
293+ { /* Activity Photo */ }
294+ { ( ( ) => {
295+ const photoUrl = getStravaPhotoUrl ( stravaActivity ) ;
296+ const photoCount =
297+ stravaActivity . total_photo_count ??
298+ stravaActivity . photos ?. count ??
299+ 0 ;
300+
301+ if ( ! photoUrl && photoCount === 0 ) return null ;
302+
303+ return (
304+ < div className = "mt-3" >
305+ { photoUrl ? (
306+ < div className = "relative inline-block" >
307+ < img
308+ src = { photoUrl }
309+ alt = { `Photo from ${ stravaActivity . name } ` }
310+ className = "h-24 w-auto rounded border border-zinc-700 object-cover"
311+ />
312+ { photoCount > 1 && (
313+ < div className = "absolute right-1 top-1 flex items-center gap-1 rounded-full bg-black/70 px-1.5 py-0.5 text-[10px] text-zinc-200" >
314+ < Camera className = "h-2.5 w-2.5" />
315+ { photoCount }
316+ </ div >
317+ ) }
318+ </ div >
319+ ) : (
320+ < div className = "flex items-center gap-1.5 text-sm text-zinc-500" >
321+ < Camera className = "h-3.5 w-3.5" />
322+ < span >
323+ { photoCount } photo{ photoCount !== 1 ? "s" : "" } (could not load preview)
324+ </ span >
325+ </ div >
326+ ) }
327+ </ div >
328+ ) ;
329+ } ) ( ) }
330+
252331 { /* Metrics Row */ }
253332 < div className = "mt-3 flex flex-wrap gap-4 text-sm" >
254333 { stravaActivity . distance && (
@@ -333,6 +412,28 @@ export function StravaPreviewClient({
333412 </ div >
334413 ) }
335414 </ div >
415+
416+ { /* Raw JSON Toggle */ }
417+ < div className = "mt-3 border-t border-zinc-800 pt-3" >
418+ < button
419+ type = "button"
420+ onClick = { ( ) => toggleJsonExpanded ( stravaActivity . id ) }
421+ className = "flex items-center gap-1.5 text-xs text-zinc-500 transition-colors hover:text-zinc-300"
422+ >
423+ < Code className = "h-3.5 w-3.5" />
424+ < span > Raw JSON</ span >
425+ { expandedJsonIds . has ( stravaActivity . id ) ? (
426+ < ChevronUp className = "h-3 w-3" />
427+ ) : (
428+ < ChevronDown className = "h-3 w-3" />
429+ ) }
430+ </ button >
431+ { expandedJsonIds . has ( stravaActivity . id ) && (
432+ < pre className = "mt-2 max-h-80 overflow-auto rounded-md border border-zinc-800 bg-zinc-950 p-3 text-xs text-zinc-400" >
433+ { JSON . stringify ( stravaActivity , null , 2 ) }
434+ </ pre >
435+ ) }
436+ </ div >
336437 </ div >
337438 ) ) }
338439 </ div >
0 commit comments