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,
}
})()