All tweaks added
Build and push images / build-backend (push) Successful in 33s
Build and push images / build-worker (push) Successful in 32s
Build and push images / build-frontend (push) Failing after 6s

This commit is contained in:
2026-06-06 18:10:35 +01:00
parent 043b3b7269
commit ec5a01d12a
92 changed files with 7517 additions and 784 deletions
@@ -0,0 +1,26 @@
import axios from 'axios'
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || '/api',
})
api.interceptors.request.use((config) => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
api.interceptors.response.use(
(res) => res,
(err) => {
if (err.response?.status === 401) {
localStorage.removeItem('token')
window.location.href = '/login'
}
return Promise.reject(err)
}
)
export default api
@@ -0,0 +1,94 @@
export function formatDuration(seconds) {
if (!seconds) return '--'
const h = Math.floor(seconds / 3600)
const m = Math.floor((seconds % 3600) / 60)
const s = Math.floor(seconds % 60)
if (h > 0) return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
return `${m}:${String(s).padStart(2, '0')}`
}
export function formatPace(speedMs, sportType = 'running') {
if (!speedMs || speedMs <= 0) return '--'
if (sportType === 'cycling') {
return `${(speedMs * 3.6).toFixed(1)} km/h`
}
const secsPerKm = 1000 / speedMs
const mins = Math.floor(secsPerKm / 60)
const secs = Math.floor(secsPerKm % 60)
return `${mins}:${String(secs).padStart(2, '0')} /km`
}
export function formatDistance(metres) {
if (!metres) return '--'
if (metres >= 1000) return `${(metres / 1000).toFixed(2)} km`
return `${Math.round(metres)} m`
}
export function formatElevation(metres) {
if (metres == null) return '--'
return `${Math.round(metres)} m`
}
export function formatHeartRate(bpm) {
if (!bpm) return '--'
return `${Math.round(bpm)} bpm`
}
export function formatSleep(seconds) {
if (!seconds) return '--'
const h = Math.floor(seconds / 3600)
const m = Math.round((seconds % 3600) / 60)
return `${h}h ${m}m`
}
export function formatWeight(kg) {
if (!kg) return '--'
return `${kg.toFixed(1)} kg`
}
export function formatDate(dateStr) {
if (!dateStr) return '--'
return new Date(dateStr).toLocaleDateString('en-GB', {
day: 'numeric', month: 'short', year: 'numeric',
})
}
export function formatDateTime(dateStr) {
if (!dateStr) return '--'
return new Date(dateStr).toLocaleDateString('en-GB', {
day: 'numeric', month: 'short', year: 'numeric',
hour: '2-digit', minute: '2-digit',
})
}
export function formatCadence(value, sportType) {
if (!value) return '--'
// Garmin stores running cadence as steps per minute / 2 (one foot)
// We need to double it to get total steps per minute (both feet)
if (sportType === 'running' || sportType === 'hiking' || sportType === 'walking') {
return `${Math.round(value * 2)} spm`
}
// Cycling is already in rpm
return `${Math.round(value)} rpm`
}
export function hrZoneColor(zone) {
const colors = { z1: '#60a5fa', z2: '#34d399', z3: '#fbbf24', z4: '#f97316', z5: '#f43f5e' }
return colors[zone] || '#9ca3af'
}
export function sportIcon(sportType) {
const icons = {
running: '🏃', cycling: '🚴', hiking: '🥾',
walking: '🚶', other: '⚡',
}
return icons[sportType?.toLowerCase()] || '⚡'
}
export function sportColor(sportType) {
const colors = {
running: '#3b82f6', cycling: '#f97316',
hiking: '#84cc16', walking: '#a78bfa', other: '#6b7280',
}
return colors[sportType?.toLowerCase()] || '#6b7280'
}