diff --git a/edge-apps/strava-club-leaderboard/README.md b/edge-apps/strava-club-leaderboard/README.md index 2b6e9b090..289319dc7 100644 --- a/edge-apps/strava-club-leaderboard/README.md +++ b/edge-apps/strava-club-leaderboard/README.md @@ -14,6 +14,7 @@ A beautiful, real-time leaderboard for Strava clubs that displays member ranking - **Caching**: Efficient data caching to reduce API calls - **Error handling**: Graceful error states with helpful messages - **Responsive**: Adapts to different screen sizes and resolutions +- **Unit Type Configuration**: Users can choose whether to display units in Imperial or Metric. ## Prerequisites diff --git a/edge-apps/strava-club-leaderboard/screenly.yml b/edge-apps/strava-club-leaderboard/screenly.yml index 4d30ebc7b..6f7f2673c 100644 --- a/edge-apps/strava-club-leaderboard/screenly.yml +++ b/edge-apps/strava-club-leaderboard/screenly.yml @@ -35,4 +35,20 @@ settings: title: Strava Refresh Token optional: false help_text: | - Enter your Strava Refresh Token from https://www.strava.com/settings/api (keep this secure). \ No newline at end of file + Enter your Strava Refresh Token from https://www.strava.com/settings/api (keep this secure). + unit_type: + type: string + title: Unit System + default_value: metric + optional: true + help_text: + properties: + type: select + help_text: | + Choose the unit system for displaying distance and elevation. Options: "metric" (km, m) or "imperial" (mi, ft). + options: + - label: Metric + value: metric + - label: Imperial + value: imperial + schema_version: 1 diff --git a/edge-apps/strava-club-leaderboard/screenly_qc.yml b/edge-apps/strava-club-leaderboard/screenly_qc.yml index 6660be05d..6d677cc4f 100644 --- a/edge-apps/strava-club-leaderboard/screenly_qc.yml +++ b/edge-apps/strava-club-leaderboard/screenly_qc.yml @@ -36,3 +36,19 @@ settings: optional: false help_text: | Enter your Strava Refresh Token from https://www.strava.com/settings/api (keep this secure). + unit_type: + type: string + title: Unit System + default_value: metric + optional: true + help_text: + properties: + type: select + help_text: | + Choose the unit system for displaying distance and elevation. Options: "metric" (km, m) or "imperial" (mi, ft). + options: + - label: Metric + value: metric + - label: Imperial + value: imperial + schema_version: 1 diff --git a/edge-apps/strava-club-leaderboard/static/js/api.js b/edge-apps/strava-club-leaderboard/static/js/api.js index 15b1928b1..c27f26c7a 100644 --- a/edge-apps/strava-club-leaderboard/static/js/api.js +++ b/edge-apps/strava-club-leaderboard/static/js/api.js @@ -1,4 +1,4 @@ -/* eslint-disable no-unused-vars, no-undef, no-useless-catch */ +/* eslint-disable no-unused-vars, no-useless-catch */ /* global screenly, StravaCache */ @@ -6,12 +6,6 @@ window.StravaAPI = (function () { 'use strict' - // Debug: Check if StravaCache is properly loaded - console.log('StravaAPI loading. StravaCache status:', { - exists: typeof StravaCache !== 'undefined', - functions: typeof StravaCache === 'object' ? Object.keys(StravaCache) : 'N/A' - }) - // Configuration const CONFIG = { STRAVA_API_BASE: 'https://www.strava.com/api/v3', @@ -19,7 +13,9 @@ window.StravaAPI = (function () { MAX_ACTIVITIES_PER_REQUEST: 200, RETRY_ATTEMPTS: 3, RETRY_DELAY: 1000, - TOKEN_REFRESH_BUFFER: 300 // Refresh token 5 minutes before expiry + TOKEN_REFRESH_BUFFER: 300, // Refresh token 5 minutes before expiry + RATE_LIMIT_RETRY_DELAY: 60000, // Wait 60 seconds on rate limit (429) + RATE_LIMIT_MAX_RETRIES: 2, // Max retries for rate limit errors } // Token management state @@ -27,7 +23,7 @@ window.StravaAPI = (function () { let tokenExpiresAt = null // Internal state for token expiry // Helper function to calculate token expiry information - function getTokenExpiryInfo (includeExtended = false) { + function getTokenExpiryInfo(includeExtended = false) { if (!tokenExpiresAt) return null const now = Math.floor(Date.now() / 1000) @@ -37,20 +33,21 @@ window.StravaAPI = (function () { minutes: Math.round(secondsUntilExpiry / 60), hours: Math.round(secondsUntilExpiry / 3600), expiryTime: new Date(tokenExpiresAt * 1000).toLocaleString(), - expiryTimeISO: new Date(tokenExpiresAt * 1000).toISOString() + expiryTimeISO: new Date(tokenExpiresAt * 1000).toISOString(), } if (includeExtended) { expiryInfo.days = Math.round(secondsUntilExpiry / 86400) expiryInfo.isExpired = secondsUntilExpiry <= 0 - expiryInfo.needsRefresh = secondsUntilExpiry <= CONFIG.TOKEN_REFRESH_BUFFER + expiryInfo.needsRefresh = + secondsUntilExpiry <= CONFIG.TOKEN_REFRESH_BUFFER } return expiryInfo } // Helper function to log token expiry information - function logTokenExpiry (options = {}) { + function logTokenExpiry(options = {}) { const expiryInfo = getTokenExpiryInfo(options.includeExtended) if (!expiryInfo) return null @@ -69,20 +66,19 @@ window.StravaAPI = (function () { delete logInfo.days } - console.log('โฐ Token will expire in:', logInfo) return expiryInfo } // Check if token needs refresh (expires within buffer time) - function needsTokenRefresh () { + function needsTokenRefresh() { if (!tokenExpiresAt) return false // If no expiry time set, we'll handle 401s reactively const now = Math.floor(Date.now() / 1000) - return (tokenExpiresAt - now) <= CONFIG.TOKEN_REFRESH_BUFFER + return tokenExpiresAt - now <= CONFIG.TOKEN_REFRESH_BUFFER } // Check if token is expired - function isTokenExpired () { + function isTokenExpired() { if (!tokenExpiresAt) return false // If no expiry time set, we'll handle 401s reactively const now = Math.floor(Date.now() / 1000) @@ -90,7 +86,7 @@ window.StravaAPI = (function () { } // Refresh access token using refresh token - async function refreshAccessToken () { + async function refreshAccessToken() { // If there's already a refresh in progress, wait for it if (tokenRefreshPromise) { return tokenRefreshPromise @@ -101,29 +97,31 @@ window.StravaAPI = (function () { const clientSecret = screenly.settings.client_secret if (!refreshToken || !clientId || !clientSecret) { - throw new Error('Missing refresh token or client credentials. Please reconfigure your Strava authentication.') + throw new Error( + 'Missing refresh token or client credentials. Please reconfigure your Strava authentication.', + ) } - console.log('Refreshing Strava access token...') - tokenRefreshPromise = (async () => { try { const response = await fetch(`${CONFIG.STRAVA_OAUTH_BASE}/token`, { method: 'POST', headers: { - 'Content-Type': 'application/x-www-form-urlencoded' + 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ client_id: clientId, client_secret: clientSecret, refresh_token: refreshToken, - grant_type: 'refresh_token' - }) + grant_type: 'refresh_token', + }), }) if (!response.ok) { const errorData = await response.json().catch(() => ({})) - throw new Error(`Token refresh failed: ${response.status} - ${errorData.message || response.statusText}`) + throw new Error( + `Token refresh failed: ${response.status} - ${errorData.message || response.statusText}`, + ) } const tokenData = await response.json() @@ -137,24 +135,13 @@ window.StravaAPI = (function () { const expiryDate = new Date(tokenData.expires_at * 1000) const now = new Date() - const secondsUntilExpiry = tokenData.expires_at - Math.floor(Date.now() / 1000) - - console.log('๐ŸŽ‰ Access token refreshed successfully!') - console.log('โฐ Token Details:', { - expiresAt: expiryDate.toISOString(), - expiresAtLocal: expiryDate.toLocaleString(), - currentTime: now.toISOString(), - secondsUntilExpiry, - minutesUntilExpiry: Math.round(secondsUntilExpiry / 60), - hoursUntilExpiry: Math.round(secondsUntilExpiry / 3600) - }) + const secondsUntilExpiry = + tokenData.expires_at - Math.floor(Date.now() / 1000) // Clear cache on token refresh to avoid stale data if (StravaCache.clearCacheOnAuthChange) { StravaCache.clearCacheOnAuthChange() - console.log('๐Ÿงน Cache cleared due to token refresh (clearCacheOnAuthChange)') } else if (StravaCache.clearCache) { - console.log('๐Ÿงน Using fallback cache clear method') StravaCache.clearCache() } @@ -171,28 +158,19 @@ window.StravaAPI = (function () { } // Probe current token to check if it's valid and get expiry info - async function probeCurrentToken () { + async function probeCurrentToken() { try { // Make a simple API call to check token validity const response = await fetch(`${CONFIG.STRAVA_API_BASE}/athlete`, { headers: { Authorization: `Bearer ${screenly.settings.access_token}`, - 'Content-Type': 'application/json' - } + 'Content-Type': 'application/json', + }, }) if (response.ok) { - console.log('โœ… Current access token is valid') - // If expires_at is not set, we can't determine exact expiry from this call - // but we know the token works for now - if (!tokenExpiresAt) { - console.log('โš ๏ธ Token expiry time not set - will handle expiry reactively when 401 occurs') - } else { - logTokenExpiry({ includeSummary: false }) - } return true } else if (response.status === 401) { - console.log('โŒ Current access token is expired, will attempt refresh') return false } else { console.warn('Unexpected response from token probe:', response.status) @@ -205,33 +183,21 @@ window.StravaAPI = (function () { } // Ensure valid access token - async function ensureValidToken () { + async function ensureValidToken() { const now = Math.floor(Date.now() / 1000) - console.log('๐Ÿ” Token validation check:', { - hasExpiryTime: !!tokenExpiresAt, - expiresAt: tokenExpiresAt ? new Date(tokenExpiresAt * 1000).toISOString() : 'Unknown', - currentTime: new Date(now * 1000).toISOString(), - secondsUntilExpiry: tokenExpiresAt ? tokenExpiresAt - now : 'Unknown', - needsRefresh: tokenExpiresAt ? needsTokenRefresh() : false, - isExpired: tokenExpiresAt ? isTokenExpired() : false - }) - - // Always show expiry details if we have them - logTokenExpiry({ includeISO: true, includeSummary: true }) // Include ISO format for detailed validation - // If we don't have expiry info, probe the current token first if (!tokenExpiresAt) { - console.log('โฐ No token expiry time available, probing current token...') const isValid = await probeCurrentToken() if (!isValid) { // Current token is invalid, try to refresh try { - console.log('๐Ÿ”„ Token invalid, attempting refresh...') await refreshAccessToken() } catch (error) { console.error('โŒ Token refresh failed during probe:', error) - throw new Error('Authentication failed. Please check your Strava credentials and try again.') + throw new Error( + 'Authentication failed. Please check your Strava credentials and try again.', + ) } } return // Exit early since we just probed/refreshed @@ -240,86 +206,96 @@ window.StravaAPI = (function () { // Check if token needs refresh or is expired (only if we have expiry info) if (needsTokenRefresh() || isTokenExpired()) { try { - console.log('๐Ÿ”„ Token needs refresh, attempting refresh...') await refreshAccessToken() } catch (error) { console.error('โŒ Token refresh failed:', error) - throw new Error('Authentication failed. Please check your Strava credentials and try again.') + throw new Error( + 'Authentication failed. Please check your Strava credentials and try again.', + ) } - } else { - console.log('โœ… Token is valid and fresh') } } - // Make authenticated request to Strava API with automatic token refresh - async function makeStravaRequest (url, options = {}) { + // Helper function to wait/sleep + function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)) + } + + // Make authenticated request to Strava API with automatic token refresh and rate limit handling + async function makeStravaRequest(url, options = {}, rateLimitRetry = 0) { // Ensure we have a valid token before making the request await ensureValidToken() - // Show current token status before making request - console.log(`๐Ÿš€ Making API request to: ${url}`) - if (tokenExpiresAt) { - const expiryInfo = getTokenExpiryInfo() - console.log('โฐ Current token expires in:', { - minutes: expiryInfo.minutes, - hours: expiryInfo.hours, - expiryTime: expiryInfo.expiryTime - }) - } else { - console.log('โฐ No token expiry time available') - } - const headers = { Authorization: `Bearer ${screenly.settings.access_token}`, 'Content-Type': 'application/json', - ...options.headers + ...options.headers, } try { const response = await fetch(url, { ...options, - headers + headers, }) + // Handle 429 Rate Limit Exceeded + if (response.status === 429) { + const retryAfter = response.headers.get('Retry-After') + const waitTime = retryAfter + ? parseInt(retryAfter, 10) * 1000 + : CONFIG.RATE_LIMIT_RETRY_DELAY + + console.warn( + `โš ๏ธ Rate limit exceeded (429). Retry attempt ${rateLimitRetry + 1}/${CONFIG.RATE_LIMIT_MAX_RETRIES}`, + ) + + if (rateLimitRetry < CONFIG.RATE_LIMIT_MAX_RETRIES) { + await sleep(waitTime) + return makeStravaRequest(url, options, rateLimitRetry + 1) + } else { + throw new Error( + 'Rate Limit Exceeded. Strava API limits reached. Please wait a few minutes and try again.', + ) + } + } + // Handle 401 Unauthorized - token might be expired if (response.status === 401) { - console.log('โŒ Received 401 Unauthorized, attempting token refresh...') - console.log('๐Ÿ” Current token state:', { - hasExpiryTime: !!tokenExpiresAt, - expiresAt: tokenExpiresAt ? new Date(tokenExpiresAt * 1000).toLocaleString() : 'Unknown', - url - }) - try { await refreshAccessToken() // Retry the request with new token const retryHeaders = { ...headers, - Authorization: `Bearer ${screenly.settings.access_token}` + Authorization: `Bearer ${screenly.settings.access_token}`, } const retryResponse = await fetch(url, { ...options, - headers: retryHeaders + headers: retryHeaders, }) if (!retryResponse.ok) { const errorData = await retryResponse.json().catch(() => ({})) - throw new Error(`Strava API error: ${retryResponse.status} - ${errorData.message || retryResponse.statusText}`) + throw new Error( + `Strava API error: ${retryResponse.status} - ${errorData.message || retryResponse.statusText}`, + ) } - console.log('โœ… API request succeeded after token refresh') return await retryResponse.json() } catch (refreshError) { console.error('Token refresh failed after 401:', refreshError) - throw new Error('Authentication failed. Please reconfigure your Strava credentials.') + throw new Error( + 'Authentication failed. Please reconfigure your Strava credentials.', + ) } } if (!response.ok) { const errorData = await response.json().catch(() => ({})) - throw new Error(`Strava API error: ${response.status} - ${errorData.message || response.statusText}`) + throw new Error( + `Strava API error: ${response.status} - ${errorData.message || response.statusText}`, + ) } const data = await response.json() @@ -330,7 +306,7 @@ window.StravaAPI = (function () { } // Fetch club details with caching - async function fetchClubDetails (clubId) { + async function fetchClubDetails(clubId) { // Check cache first - club details don't change often, so cache for 1 hour const cacheKey = StravaCache.getCacheKey ? StravaCache.getCacheKey('details', clubId) @@ -341,11 +317,8 @@ window.StravaAPI = (function () { // Proceed without caching } - console.log('๐Ÿ” Fetching club details:', { clubId, cacheKey }) - const cachedData = cacheKey ? StravaCache.getCachedData(cacheKey) : null if (cachedData) { - console.log('โœ… Club details loaded from cache using key:', cacheKey) return cachedData } @@ -355,10 +328,11 @@ window.StravaAPI = (function () { // Cache club details for 1 hour since they rarely change if (clubData && cacheKey) { - const cached = StravaCache.setCachedDataWithDuration(cacheKey, clubData, 60 * 60 * 1000) // 1 hour - if (cached) { - console.log('๐Ÿ’พ Club details cached for 1 hour using key:', cacheKey) - } + StravaCache.setCachedDataWithDuration( + cacheKey, + clubData, + 60 * 60 * 1000, + ) // 1 hour } return clubData @@ -369,7 +343,7 @@ window.StravaAPI = (function () { } // Fetch detailed activity information - async function fetchDetailedActivity (activityId) { + async function fetchDetailedActivity(activityId) { const url = `${CONFIG.STRAVA_API_BASE}/activities/${activityId}` try { const detailedActivity = await makeStravaRequest(url) @@ -380,7 +354,7 @@ window.StravaAPI = (function () { } // Fetch club activities with caching and pagination - async function fetchClubActivities (clubId, page = 1) { + async function fetchClubActivities(clubId, page = 1) { const cacheKey = StravaCache.getCacheKey ? StravaCache.getCacheKey('activities', clubId, 'recent', page) : `strava_club_activities_${clubId}_recent_${page}` @@ -390,19 +364,16 @@ window.StravaAPI = (function () { // Proceed without caching } - console.log(`๐Ÿ” Fetching club activities page ${page}:`, { clubId, page, cacheKey }) - // Check cache first const cachedData = cacheKey ? StravaCache.getCachedData(cacheKey) : null if (cachedData) { - console.log(`โœ… Club activities page ${page} loaded from cache using key:`, cacheKey) return cachedData } const url = `${CONFIG.STRAVA_API_BASE}/clubs/${clubId}/activities` const params = new URLSearchParams({ page: page.toString(), - per_page: CONFIG.MAX_ACTIVITIES_PER_REQUEST.toString() + per_page: CONFIG.MAX_ACTIVITIES_PER_REQUEST.toString(), }) try { @@ -410,7 +381,9 @@ window.StravaAPI = (function () { // Handle case where API returns non-array if (!Array.isArray(activities)) { - throw new Error(`API returned invalid response type: ${typeof activities}`) + throw new Error( + `API returned invalid response type: ${typeof activities}`, + ) } // Remove the technical warning message - we'll show a user-friendly note in the footer instead @@ -422,12 +395,9 @@ window.StravaAPI = (function () { // Return all activities without time filtering const processedActivities = activities - // Cache the processed activities (10 minutes default) + // Cache the processed activities (30 minutes default) if (cacheKey) { - const cached = StravaCache.setCachedData(cacheKey, processedActivities) - if (cached) { - console.log(`๐Ÿ’พ Club activities page ${page} cached for 10 minutes using key:`, cacheKey) - } + StravaCache.setCachedData(cacheKey, processedActivities) } return processedActivities @@ -438,7 +408,7 @@ window.StravaAPI = (function () { } // Fetch all club activities with pagination - async function fetchAllClubActivities (clubId) { + async function fetchAllClubActivities(clubId) { const allActivities = [] let page = 1 let hasMore = true @@ -459,6 +429,10 @@ window.StravaAPI = (function () { } } } catch (error) { + console.error( + `โŒ Error fetching activities page ${page}:`, + error.message, + ) hasMore = false } } @@ -467,12 +441,20 @@ window.StravaAPI = (function () { } // Process activities into leaderboard format - function processLeaderboard (activities) { + function processLeaderboard(activities) { const athleteStats = {} - activities.forEach(activity => { + activities.forEach((activity) => { + // Validate activity structure + if (!activity || !activity.athlete) { + console.warn('โš ๏ธ Skipping invalid activity:', activity) + return + } + // Handle missing athlete ID by using name as fallback - const athleteId = activity.athlete.id || `${activity.athlete.firstname}_${activity.athlete.lastname}` + const athleteId = + activity.athlete.id || + `${activity.athlete.firstname}_${activity.athlete.lastname}` const athleteName = `${activity.athlete.firstname} ${activity.athlete.lastname}` if (!athleteStats[athleteId]) { @@ -485,7 +467,8 @@ window.StravaAPI = (function () { totalTime: 0, totalElevation: 0, activityCount: 0, - activities: [] + activities: [], + latestActivityTime: null, // Track when athlete last recorded an activity } } @@ -495,30 +478,57 @@ window.StravaAPI = (function () { stats.totalElevation += activity.total_elevation_gain || 0 stats.activityCount++ stats.activities.push(activity) + + // Track the latest activity time for tiebreaker (Strava logic: who achieved distance first) + const activityTime = activity.start_date_local || activity.start_date + if (activityTime) { + const activityTimestamp = new Date(activityTime).getTime() + if ( + !stats.latestActivityTime || + activityTimestamp > stats.latestActivityTime + ) { + stats.latestActivityTime = activityTimestamp + } + } }) - // Convert to array and sort by total distance - const leaderboard = Object.values(athleteStats) - .sort((a, b) => b.totalDistance - a.totalDistance) + // Convert to array and sort using Strava's ranking logic: + // 1. Primary: Total distance (descending) - highest distance wins + // 2. Tiebreaker: Latest activity time (ascending) - who achieved their distance first wins + // 3. Final tiebreaker: Alphabetical by name + const leaderboard = Object.values(athleteStats).sort((a, b) => { + // Primary sort: total distance (descending) + if (b.totalDistance !== a.totalDistance) { + return b.totalDistance - a.totalDistance + } + + // Tiebreaker 1: Who achieved their distance first (earlier latest activity wins) + // Athletes who finished their activities earlier rank higher when distance is tied + if (a.latestActivityTime && b.latestActivityTime) { + if (a.latestActivityTime !== b.latestActivityTime) { + return a.latestActivityTime - b.latestActivityTime + } + } + + // Tiebreaker 2: Alphabetical by name + return a.name.localeCompare(b.name) + }) // Return all athletes - filtering will be handled by the main application logic return leaderboard } // Get token info for debugging - function getTokenInfo () { - console.log('๐Ÿ” Getting token info...') - + function getTokenInfo() { if (!tokenExpiresAt) { - const tokenInfo = { - status: 'Token expiry time not set - will be auto-detected on first API call', + return { + status: + 'Token expiry time not set - will be auto-detected on first API call', hasRefreshToken: !!screenly.settings.refresh_token, hasClientSecret: !!screenly.settings.client_secret, hasAccessToken: !!screenly.settings.access_token, - internalExpiryState: 'Not initialized' + internalExpiryState: 'Not initialized', } - console.log('โš ๏ธ Token info (no expiry):', tokenInfo) - return tokenInfo } const now = Math.floor(Date.now() / 1000) @@ -537,24 +547,19 @@ window.StravaAPI = (function () { needsRefresh: secondsUntilExpiry <= CONFIG.TOKEN_REFRESH_BUFFER, refreshBufferSeconds: CONFIG.TOKEN_REFRESH_BUFFER, status: 'Token expiry managed entirely in JavaScript memory', - internalExpiryState: 'Active' + internalExpiryState: 'Active', } - console.log('โฐ Token info:', tokenInfo) return tokenInfo } // Show current token expiry status (for debugging) - function showTokenExpiry () { - console.log('๐Ÿ” Manual token expiry check...') + function showTokenExpiry() { if (!tokenExpiresAt) { - console.log('โš ๏ธ No token expiry time available') return null } - const expiryInfo = getTokenExpiryInfo(true) // Include extended info - console.log('โฐ Token will expire in:', expiryInfo) - return expiryInfo + return getTokenExpiryInfo(true) // Include extended info } // Public API @@ -571,6 +576,6 @@ window.StravaAPI = (function () { getTokenExpiryInfo, showTokenExpiry, needsTokenRefresh, - isTokenExpired + isTokenExpired, } })() diff --git a/edge-apps/strava-club-leaderboard/static/js/cache.js b/edge-apps/strava-club-leaderboard/static/js/cache.js index 9bdff4e74..32c2a14e7 100644 --- a/edge-apps/strava-club-leaderboard/static/js/cache.js +++ b/edge-apps/strava-club-leaderboard/static/js/cache.js @@ -1,3 +1,5 @@ +/* eslint-disable no-unused-vars */ + /* global */ // Cache management for Strava Club Leaderboard App @@ -8,14 +10,12 @@ window.StravaCache = (function () { 'use strict' - console.log('StravaCache module loading...') - // Configuration - const CACHE_DURATION = 10 * 60 * 1000 // 10 minutes - Conservative caching for frequent updates + const CACHE_DURATION = 30 * 60 * 1000 // 30 minutes - Balance between freshness and rate limits const CACHE_NAMESPACE = 'strava_club_' // Namespace for cache keys // Get cached data with expiration check - function getCachedData (key) { + function getCachedData(key) { try { const cached = localStorage.getItem(key) if (cached) { @@ -52,7 +52,7 @@ window.StravaCache = (function () { } // Helper function for cache write with error handling - function writeToCache (key, cacheEntry) { + function writeToCache(key, cacheEntry) { try { localStorage.setItem(key, JSON.stringify(cacheEntry)) return true @@ -65,7 +65,6 @@ window.StravaCache = (function () { // Try again after clearing try { localStorage.setItem(key, JSON.stringify(cacheEntry)) - console.log('๐Ÿ’พ Cache write successful after cleanup') return true } catch (retryError) { console.warn('โŒ Failed to cache data after cleanup:', retryError) @@ -79,28 +78,31 @@ window.StravaCache = (function () { } // Set cached data with default duration - function setCachedData (key, data) { + function setCachedData(key, data) { if (!key || data === undefined) { - console.warn('โŒ Invalid cache parameters:', { key, hasData: data !== undefined }) + console.warn('โŒ Invalid cache parameters:', { + key, + hasData: data !== undefined, + }) return false } const cacheEntry = { data, timestamp: Date.now(), - version: 1 // Cache version for future migrations + version: 1, // Cache version for future migrations } return writeToCache(key, cacheEntry) } // Set cached data with custom duration - function setCachedDataWithDuration (key, data, duration) { + function setCachedDataWithDuration(key, data, duration) { if (!key || data === undefined || !duration || duration <= 0) { console.warn('โŒ Invalid cache parameters:', { key, hasData: data !== undefined, - duration + duration, }) return false } @@ -109,14 +111,14 @@ window.StravaCache = (function () { data, timestamp: Date.now(), customDuration: duration, - version: 1 + version: 1, } return writeToCache(key, cacheEntry) } // Clear all Strava-related cache - function clearCache () { + function clearCache() { const keysToRemove = [] // Collect all matching keys first (avoid modifying during iteration) @@ -128,48 +130,44 @@ window.StravaCache = (function () { } // Remove collected keys - keysToRemove.forEach(key => { + keysToRemove.forEach((key) => { try { localStorage.removeItem(key) } catch (error) { console.warn('Failed to remove cache key:', key, error) } }) - - console.log(`๐Ÿงน Cleared ${keysToRemove.length} cache entries`) } // Clear cache on authentication change (token refresh/change) - function clearCacheOnAuthChange () { - console.log('Clearing cache due to authentication change') + function clearCacheOnAuthChange() { clearCache() } // Clear cache for specific club - function clearCacheForClub (clubId) { + function clearCacheForClub(clubId) { const keysToRemove = [] // Find all cache keys for this club (activities and details) for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i) - if (key && ( - key.startsWith(`${CACHE_NAMESPACE}activities_${clubId}_`) || - key === `${CACHE_NAMESPACE}details_${clubId}` - )) { + if ( + key && + (key.startsWith(`${CACHE_NAMESPACE}activities_${clubId}_`) || + key === `${CACHE_NAMESPACE}details_${clubId}`) + ) { keysToRemove.push(key) } } // Remove found keys - keysToRemove.forEach(key => { + keysToRemove.forEach((key) => { localStorage.removeItem(key) }) - - console.log(`Cleared ${keysToRemove.length} cache entries for club ${clubId}`) } // Get cache size and statistics - function getCacheStats () { + function getCacheStats() { const cacheInfo = [] let totalSize = 0 @@ -190,7 +188,7 @@ window.StravaCache = (function () { age: parsed.timestamp ? Date.now() - parsed.timestamp : 0, version: parsed.version || 0, hasCustomDuration: !!parsed.customDuration, - customDuration: parsed.customDuration + customDuration: parsed.customDuration, }) } catch (error) { // Handle corrupted entries @@ -199,7 +197,7 @@ window.StravaCache = (function () { size: 0, timestamp: 0, age: 0, - corrupted: true + corrupted: true, }) } } @@ -208,19 +206,19 @@ window.StravaCache = (function () { return { totalKeys: cacheInfo.length, totalSize, - totalSizeMB: Math.round(totalSize / 1024 / 1024 * 100) / 100, - cacheInfo: cacheInfo.sort((a, b) => b.age - a.age) // Sort by age, oldest first + totalSizeMB: Math.round((totalSize / 1024 / 1024) * 100) / 100, + cacheInfo: cacheInfo.sort((a, b) => b.age - a.age), // Sort by age, oldest first } } // Manage cache size by removing old entries - function manageCacheSize (maxEntries = 50) { + function manageCacheSize(maxEntries = 50) { const stats = getCacheStats() let removedCount = 0 // Remove corrupted entries first - const corruptedEntries = stats.cacheInfo.filter(item => item.corrupted) - corruptedEntries.forEach(item => { + const corruptedEntries = stats.cacheInfo.filter((item) => item.corrupted) + corruptedEntries.forEach((item) => { try { localStorage.removeItem(item.key) removedCount++ @@ -230,10 +228,10 @@ window.StravaCache = (function () { }) // Then remove oldest entries if still over limit - const validEntries = stats.cacheInfo.filter(item => !item.corrupted) + const validEntries = stats.cacheInfo.filter((item) => !item.corrupted) if (validEntries.length > maxEntries) { const toRemove = validEntries.slice(maxEntries) - toRemove.forEach(item => { + toRemove.forEach((item) => { try { localStorage.removeItem(item.key) removedCount++ @@ -243,15 +241,11 @@ window.StravaCache = (function () { }) } - if (removedCount > 0) { - console.log(`๐Ÿงน Removed ${removedCount} cache entries (${corruptedEntries.length} corrupted)`) - } - return removedCount } // Check if cache is healthy (not too many entries, not too large) - function checkCacheHealth () { + function checkCacheHealth() { const stats = getCacheStats() const maxSize = 5 * 1024 * 1024 // 5MB limit const maxEntries = 50 // Reduced for more frequent cleanup @@ -259,7 +253,9 @@ window.StravaCache = (function () { const issues = [] if (stats.totalSize > maxSize) { - issues.push(`Cache size too large: ${Math.round(stats.totalSize / 1024 / 1024)}MB`) + issues.push( + `Cache size too large: ${Math.round(stats.totalSize / 1024 / 1024)}MB`, + ) } if (stats.totalKeys > maxEntries) { @@ -269,23 +265,26 @@ window.StravaCache = (function () { return { healthy: issues.length === 0, issues, - stats + stats, } } // Generate cache key with namespace and validation - function getCacheKey (type, ...parts) { + function getCacheKey(type, ...parts) { if (!type) { console.warn('โŒ Cache key type is required') return null } // Filter out null/undefined parts and convert to string - const validParts = parts.filter(part => part !== null && part !== undefined) - .map(part => String(part)) + const validParts = parts + .filter((part) => part !== null && part !== undefined) + .map((part) => String(part)) if (validParts.length === 0) { - console.warn('โŒ Cache key requires at least one additional part beyond type') + console.warn( + 'โŒ Cache key requires at least one additional part beyond type', + ) return null } @@ -300,7 +299,7 @@ window.StravaCache = (function () { } // Clear cache on quota exceeded error - function handleQuotaExceededError () { + function handleQuotaExceededError() { console.warn('๐Ÿ’พ localStorage quota exceeded, performing cleanup...') // First try to clean up old entries @@ -314,23 +313,11 @@ window.StravaCache = (function () { } // Add cache cleanup with detailed reporting - function cleanupCache () { + function cleanupCache() { const statsBefore = getCacheStats() const removedCount = manageCacheSize() const statsAfter = getCacheStats() - console.log('๐Ÿงน Cache cleanup report:', { - before: { - entries: statsBefore.totalKeys, - sizeMB: statsBefore.totalSizeMB - }, - after: { - entries: statsAfter.totalKeys, - sizeMB: statsAfter.totalSizeMB - }, - removed: removedCount - }) - return { statsBefore, statsAfter, removedCount } } @@ -347,17 +334,7 @@ window.StravaCache = (function () { checkCacheHealth, getCacheKey, handleQuotaExceededError, - cleanupCache - } - - console.log('StravaCache module loaded with functions:', Object.keys(cacheAPI)) - - // Test the getCacheKey function immediately - try { - const testKey = getCacheKey('test', 'key') - console.log('getCacheKey test successful:', testKey) - } catch (error) { - console.error('getCacheKey test failed:', error) + cleanupCache, } return cacheAPI diff --git a/edge-apps/strava-club-leaderboard/static/js/main.js b/edge-apps/strava-club-leaderboard/static/js/main.js index aa35b9123..1f4ca398f 100644 --- a/edge-apps/strava-club-leaderboard/static/js/main.js +++ b/edge-apps/strava-club-leaderboard/static/js/main.js @@ -1,14 +1,14 @@ /* global screenly, StravaUtils, StravaCache, StravaAPI, StravaUI */ // Strava Club Leaderboard App - Main Application Logic -(function () { +;(function () { 'use strict' // Configuration const CONFIG = { - REFRESH_INTERVAL: 15 * 60 * 1000, // 15 minutes - Conservative for API rate limits (600 req/15min) + REFRESH_INTERVAL: 30 * 60 * 1000, // 30 minutes - Conservative for API rate limits (100 req/15min, 1000/day) RETRY_ATTEMPTS: 3, - RETRY_DELAY: 1000 + RETRY_DELAY: 1000, } // State management @@ -18,18 +18,18 @@ activities: [], leaderboard: [], lastUpdate: null, - refreshTimer: null + refreshTimer: null, // Note: Time filtering is never available due to Strava Club Activities API limitations } // Helper function to get athlete count based on screen orientation - function getAthleteCountForOrientation () { + function getAthleteCountForOrientation() { const isLandscape = window.innerWidth > window.innerHeight return isLandscape ? 6 : 14 // 6 for landscape, 14 for portrait } // Re-render leaderboard with appropriate athlete count for current orientation - function updateLeaderboardForOrientation () { + function updateLeaderboardForOrientation() { if (appState.leaderboard && appState.leaderboard.length > 0) { const athleteCount = getAthleteCountForOrientation() StravaUI.renderLeaderboard(appState.leaderboard.slice(0, athleteCount)) @@ -43,7 +43,7 @@ } // Main application logic - async function loadLeaderboard () { + async function loadLeaderboard() { if (appState.isLoading) return appState.isLoading = true @@ -58,12 +58,16 @@ // Use real Strava API const clubId = screenly.settings.club_id if (!clubId) { - throw new Error('Club ID is required. Please configure your Strava club ID.') + throw new Error( + 'Club ID is required. Please configure your Strava club ID.', + ) } const accessToken = screenly.settings.access_token if (!accessToken) { - throw new Error('Access token is required. Please configure your Strava access token.') + throw new Error( + 'Access token is required. Please configure your Strava access token.', + ) } // Fetch club details and update logo @@ -72,6 +76,8 @@ // Note: Don't clear cache here anymore - let it expire naturally or clear on token refresh // This reduces API calls and respects rate limits better + // Note: Strava Club Activities API does not return date fields, + // so time-based filtering is not possible. Showing all recent activities. const activities = await StravaAPI.fetchAllClubActivities(clubId) // Update club logo @@ -88,7 +94,9 @@ // Update UI StravaUI.updateStats(activities, leaderboard) - StravaUI.renderLeaderboard(leaderboard.slice(0, getAthleteCountForOrientation())) // Responsive athlete count + StravaUI.renderLeaderboard( + leaderboard.slice(0, getAthleteCountForOrientation()), + ) // Responsive athlete count StravaUI.updateLastUpdated() StravaUI.updateStatsLabels() StravaUI.updateLeaderboardTitle() @@ -114,7 +122,7 @@ } // Start automatic refresh timer - function startRefreshTimer () { + function startRefreshTimer() { if (appState.refreshTimer) { clearInterval(appState.refreshTimer) } @@ -125,7 +133,7 @@ } // Stop refresh timer - function stopRefreshTimer () { + function stopRefreshTimer() { if (appState.refreshTimer) { clearInterval(appState.refreshTimer) appState.refreshTimer = null @@ -133,16 +141,8 @@ } // Initialize application - async function init () { + async function init() { try { - // Debug: Check if all modules are loaded - console.log('App initialization. Module status:', { - StravaCache: typeof StravaCache !== 'undefined', - StravaAPI: typeof StravaAPI !== 'undefined', - StravaUI: typeof StravaUI !== 'undefined', - StravaUtils: typeof StravaUtils !== 'undefined' - }) - // Initialize UI with default elements StravaUI.initializeUI() @@ -155,12 +155,10 @@ // Manage cache size periodically StravaCache.manageCacheSize() - // Check cache health and log status + // Check cache health const cacheHealth = StravaCache.checkCacheHealth() - if (cacheHealth.healthy) { - console.log('Cache is healthy:', cacheHealth.stats) - } else { - console.warn('Cache health issues:', cacheHealth.issues) + if (!cacheHealth.healthy) { + StravaCache.manageCacheSize() } } catch (error) { console.error('Failed to initialize app:', error) @@ -169,7 +167,7 @@ } // Cleanup function - function cleanup () { + function cleanup() { stopRefreshTimer() // Remove event listeners @@ -227,6 +225,7 @@ // Orientation-responsive athlete count functionality getAthleteCountForOrientation, updateLeaderboardForOrientation, - getCurrentOrientation: () => window.innerWidth > window.innerHeight ? 'landscape' : 'portrait' + getCurrentOrientation: () => + window.innerWidth > window.innerHeight ? 'landscape' : 'portrait', } })() diff --git a/edge-apps/strava-club-leaderboard/static/js/ui.js b/edge-apps/strava-club-leaderboard/static/js/ui.js index acf421d79..2576dc801 100644 --- a/edge-apps/strava-club-leaderboard/static/js/ui.js +++ b/edge-apps/strava-club-leaderboard/static/js/ui.js @@ -7,7 +7,7 @@ window.StravaUI = (function () { 'use strict' // State management functions - function showLoading () { + function showLoading() { const loadingEl = document.getElementById('loading') const errorEl = document.getElementById('error') const leaderboardEl = document.getElementById('leaderboard') @@ -17,7 +17,7 @@ window.StravaUI = (function () { if (leaderboardEl) leaderboardEl.style.display = 'none' } - function showError (message) { + function showError(message) { const loadingEl = document.getElementById('loading') const errorEl = document.getElementById('error') const leaderboardEl = document.getElementById('leaderboard') @@ -29,7 +29,7 @@ window.StravaUI = (function () { if (errorMessageEl) errorMessageEl.textContent = message } - function showLeaderboard () { + function showLeaderboard() { const loadingEl = document.getElementById('loading') const errorEl = document.getElementById('error') const leaderboardEl = document.getElementById('leaderboard') @@ -40,7 +40,7 @@ window.StravaUI = (function () { } // Update club logo and title - function updateClubLogo (clubData) { + function updateClubLogo(clubData) { const logoImage = document.querySelector('.logo-image') const logoText = document.querySelector('.logo-text') @@ -68,7 +68,7 @@ window.StravaUI = (function () { } // Update last updated time - function updateLastUpdated () { + function updateLastUpdated() { const lastUpdatedEl = document.getElementById('last-updated') if (lastUpdatedEl) { const textEl = lastUpdatedEl.querySelector('.last-updated-text') @@ -78,14 +78,14 @@ window.StravaUI = (function () { const updatedText = StravaUtils.getLocalizedText('updated', locale) textEl.textContent = `${updatedText}: ${now.toLocaleTimeString(locale, { hour: '2-digit', - minute: '2-digit' + minute: '2-digit', })}` } } } // Update statistics display - function updateStats (activities, leaderboard) { + function updateStats(activities, leaderboard) { const totalActivitiesEl = document.getElementById('total-activities') const totalDistanceEl = document.getElementById('total-distance') @@ -96,27 +96,34 @@ window.StravaUI = (function () { } if (totalDistanceEl) { - const totalDistance = activities.reduce((sum, activity) => sum + (activity.distance || 0), 0) + const totalDistance = activities.reduce( + (sum, activity) => sum + (activity.distance || 0), + 0, + ) totalDistanceEl.textContent = StravaUtils.formatDistance(totalDistance) } } // Update labels for different contexts - function updateStatsLabels () { - const statsItems = document.querySelectorAll('#leaderboard-stats .stats-item') + function updateStatsLabels() { + const statsItems = document.querySelectorAll( + '#leaderboard-stats .stats-item', + ) if (statsItems.length >= 2) { const totalActivitiesLabel = statsItems[0].querySelector('.stats-label') const totalDistanceLabel = statsItems[1].querySelector('.stats-label') // Always show "Recent" labels since time filtering isn't available - if (totalActivitiesLabel) totalActivitiesLabel.textContent = 'Recent Activities:' - if (totalDistanceLabel) totalDistanceLabel.textContent = 'Recent Distance:' + if (totalActivitiesLabel) + totalActivitiesLabel.textContent = 'Recent Activities:' + if (totalDistanceLabel) + totalDistanceLabel.textContent = 'Recent Distance:' } } // Update leaderboard title - function updateLeaderboardTitle () { + function updateLeaderboardTitle() { const title = document.getElementById('leaderboard-title') if (title) { // Always show "Most Active Recent Athletes" since time filtering isn't available @@ -125,7 +132,7 @@ window.StravaUI = (function () { } // Render the leaderboard - function renderLeaderboard (leaderboard) { + function renderLeaderboard(leaderboard) { const container = document.getElementById('leaderboard-list') if (!container) return @@ -137,7 +144,8 @@ window.StravaUI = (function () { item.className = `leaderboard-item${rank <= 3 ? ' leaderboard-item-top-3' : ''}` const rankBadge = document.createElement('div') - const rankClass = rank <= 3 ? `leaderboard-rank-${rank}` : 'leaderboard-rank-default' + const rankClass = + rank <= 3 ? `leaderboard-rank-${rank}` : 'leaderboard-rank-default' rankBadge.className = `leaderboard-rank ${rankClass}` rankBadge.textContent = rank @@ -153,9 +161,10 @@ window.StravaUI = (function () { const locale = StravaUtils.getUserLocale() const formatter = new Intl.NumberFormat(locale) const formattedCount = formatter.format(athlete.activityCount) - const activityText = athlete.activityCount === 1 - ? StravaUtils.getLocalizedText('activity', locale) - : StravaUtils.getLocalizedText('activities', locale) + const activityText = + athlete.activityCount === 1 + ? StravaUtils.getLocalizedText('activity', locale) + : StravaUtils.getLocalizedText('activities', locale) activityCount.textContent = `${formattedCount} ${activityText}` athleteInfo.appendChild(name) @@ -191,7 +200,10 @@ window.StravaUI = (function () { // Avg/Activity stat const avgStat = document.createElement('div') avgStat.className = 'leaderboard-stat' - const avgDistance = athlete.activityCount > 0 ? athlete.totalDistance / athlete.activityCount : 0 + const avgDistance = + athlete.activityCount > 0 + ? athlete.totalDistance / athlete.activityCount + : 0 avgStat.innerHTML = ` Avg/Activity ${StravaUtils.formatDistance(avgDistance)} @@ -211,7 +223,7 @@ window.StravaUI = (function () { } // Initialize default UI elements - function initializeUI () { + function initializeUI() { // Initialize with default logo and text const logoImage = document.querySelector('.logo-image') const logoText = document.querySelector('.logo-text') @@ -229,7 +241,7 @@ window.StravaUI = (function () { } // Reset time filtering state - function resetTimeFilteringState () { + function resetTimeFilteringState() { // Time filtering is never available due to API limitations // Remove any existing warning messages @@ -253,6 +265,6 @@ window.StravaUI = (function () { updateLeaderboardTitle, renderLeaderboard, initializeUI, - resetTimeFilteringState + resetTimeFilteringState, } })() diff --git a/edge-apps/strava-club-leaderboard/static/js/utils.js b/edge-apps/strava-club-leaderboard/static/js/utils.js index 216f77ab7..76d80fa4e 100644 --- a/edge-apps/strava-club-leaderboard/static/js/utils.js +++ b/edge-apps/strava-club-leaderboard/static/js/utils.js @@ -1,34 +1,53 @@ -/* global */ +/* global screenly */ // Utility functions for Strava Club Leaderboard App window.StravaUtils = (function () { 'use strict' // Locale detection - function getUserLocale () { + function getUserLocale() { return navigator.language || navigator.languages?.[0] || 'en-US' } - function getNumberFormatter (locale) { + function getNumberFormatter(locale) { return new Intl.NumberFormat(locale, { minimumFractionDigits: 0, - maximumFractionDigits: 2 + maximumFractionDigits: 2, }) } - // Check if locale uses imperial units (primarily US) - function usesImperialUnits (locale) { + // Check if imperial units should be used + // Priority: 1. Screenly setting, 2. Locale-based detection + function usesImperialUnits(locale) { + // Check if unit_type setting is configured + if ( + typeof screenly !== 'undefined' && + screenly.settings && + screenly.settings.unit_type + ) { + const unitType = screenly.settings.unit_type.toLowerCase() + if (unitType === 'imperial') { + return true + } + if (unitType === 'metric') { + return false + } + } + + // Fall back to locale-based detection // More comprehensive check for US-based locales - return locale === 'en-US' || - locale.startsWith('en-US') || - locale === 'en-LR' || - locale === 'en-MM' || - locale.startsWith('en-LR') || - locale.startsWith('en-MM') + return ( + locale === 'en-US' || + locale.startsWith('en-US') || + locale === 'en-LR' || + locale === 'en-MM' || + locale.startsWith('en-LR') || + locale.startsWith('en-MM') + ) } // Distance formatting - function formatDistance (meters) { + function formatDistance(meters) { const locale = getUserLocale() const formatter = getNumberFormatter(locale) const useImperial = usesImperialUnits(locale) @@ -55,7 +74,7 @@ window.StravaUtils = (function () { } // Time formatting - function formatTime (seconds) { + function formatTime(seconds) { const hours = Math.floor(seconds / 3600) const minutes = Math.floor((seconds % 3600) / 60) @@ -68,7 +87,7 @@ window.StravaUtils = (function () { } // Elevation formatting - function formatElevation (meters) { + function formatElevation(meters) { const locale = getUserLocale() const formatter = getNumberFormatter(locale) const useImperial = usesImperialUnits(locale) @@ -84,19 +103,23 @@ window.StravaUtils = (function () { } // Date formatting - function formatDate (dateString) { + function formatDate(dateString) { const date = new Date(dateString) const locale = getUserLocale() return date.toLocaleDateString(locale, { month: 'short', day: 'numeric', - year: 'numeric' + year: 'numeric', }) } + // Note: Time-based filtering functions were removed because + // the Strava Club Activities API does not return date fields. + // See: https://communityhub.strava.com/developers-api-7/api-club-activities-not-showing-activity-date-1777 + // Localized text - function getLocalizedText (key, locale) { + function getLocalizedText(key, locale) { const texts = { en: { updated: 'Updated', @@ -104,7 +127,7 @@ window.StravaUtils = (function () { activities: 'activities', distance: 'Distance', time: 'Time', - average: 'Average' + average: 'Average', }, es: { updated: 'Actualizado', @@ -112,7 +135,7 @@ window.StravaUtils = (function () { activities: 'actividades', distance: 'Distancia', time: 'Tiempo', - average: 'Promedio' + average: 'Promedio', }, fr: { updated: 'Mis ร  jour', @@ -120,7 +143,7 @@ window.StravaUtils = (function () { activities: 'activitรฉs', distance: 'Distance', time: 'Temps', - average: 'Moyenne' + average: 'Moyenne', }, de: { updated: 'Aktualisiert', @@ -128,7 +151,7 @@ window.StravaUtils = (function () { activities: 'Aktivitรคten', distance: 'Entfernung', time: 'Zeit', - average: 'Durchschnitt' + average: 'Durchschnitt', }, it: { updated: 'Aggiornato', @@ -136,7 +159,7 @@ window.StravaUtils = (function () { activities: 'attivitร ', distance: 'Distanza', time: 'Tempo', - average: 'Media' + average: 'Media', }, pt: { updated: 'Atualizado', @@ -144,7 +167,7 @@ window.StravaUtils = (function () { activities: 'atividades', distance: 'Distรขncia', time: 'Tempo', - average: 'Mรฉdia' + average: 'Mรฉdia', }, nl: { updated: 'Bijgewerkt', @@ -152,8 +175,8 @@ window.StravaUtils = (function () { activities: 'activiteiten', distance: 'Afstand', time: 'Tijd', - average: 'Gemiddeld' - } + average: 'Gemiddeld', + }, } const languageCode = locale.split('-')[0] @@ -162,7 +185,7 @@ window.StravaUtils = (function () { } // Activity and rank icons - function getActivityIcon (type) { + function getActivityIcon(type) { const icons = { Run: '๐Ÿƒโ€โ™‚๏ธ', Ride: '๐Ÿšดโ€โ™‚๏ธ', @@ -171,32 +194,35 @@ window.StravaUtils = (function () { Walk: '๐Ÿšถโ€โ™‚๏ธ', Workout: '๐Ÿ’ช', Yoga: '๐Ÿง˜โ€โ™‚๏ธ', - Default: '๐Ÿƒโ€โ™‚๏ธ' + Default: '๐Ÿƒโ€โ™‚๏ธ', } return icons[type] || icons.Default } - function getRankIcon (rank) { + function getRankIcon(rank) { const icons = { 1: '๐Ÿฅ‡', 2: '๐Ÿฅˆ', - 3: '๐Ÿฅ‰' + 3: '๐Ÿฅ‰', } return icons[rank] || '' } // Debug function for testing locale and units - function testLocale () { + function testLocale() { const locale = getUserLocale() const useImperial = usesImperialUnits(locale) - console.log('Current locale:', locale) - console.log('Uses imperial units:', useImperial) - console.log('Test distances:') - console.log('100m:', formatDistance(100)) - console.log('1000m:', formatDistance(1000)) - console.log('5000m:', formatDistance(5000)) - console.log('10000m:', formatDistance(10000)) - console.log('42195m:', formatDistance(42195)) // Marathon distance + return { + locale, + useImperial, + distances: { + '100m': formatDistance(100), + '1000m': formatDistance(1000), + '5000m': formatDistance(5000), + '10000m': formatDistance(10000), + '42195m': formatDistance(42195), + }, + } } // Public API @@ -211,6 +237,6 @@ window.StravaUtils = (function () { getLocalizedText, getActivityIcon, getRankIcon, - testLocale + testLocale, } })()