// DEPLOY_CONFIG: {"cron": [{"name": "weekly_running_stats_email_summary", "schedule": "0 9 * * 1", "action": "event", "config": {"event_type": "weekly_running_stats_email_summary"}}], "triggers": []} import { useEffect, useMemo, useState } from 'react'; import { useAuth, useCollection, renderChart } from '@deplixo/sdk'; import { RouteMap } from './components/RouteMap.jsx'; import { SavedRoutes } from './components/SavedRoutes.jsx'; function getDistanceKm(route) { const raw = route?.distance ?? route?.distanceKm ?? route?.length ?? route?.totalDistance; const num = Number(raw); return Number.isFinite(num) ? num : 0; } function getDateValue(route) { const raw = route?.date ?? route?.createdAt ?? route?.updatedAt ?? route?.savedAt; const date = raw ? new Date(raw) : null; return date && !Number.isNaN(date.getTime()) ? date : null; } function getPaceMinutesPerKm(route) { if (route?.paceMinutesPerKm != null) { const num = Number(route.paceMinutesPerKm); return Number.isFinite(num) ? num : null; } const timeRaw = route?.estimatedTime ?? route?.time ?? route?.duration; const distanceKm = getDistanceKm(route); const timeMinutes = Number(timeRaw); if (Number.isFinite(timeMinutes) && distanceKm > 0) { return timeMinutes / distanceKm; } return null; } function formatWeekLabel(date) { const d = new Date(date); const day = d.getDay(); const diff = d.getDate() - day + (day === 0 ? -6 : 1); const monday = new Date(d); monday.setDate(diff); monday.setHours(0, 0, 0, 0); return monday.toISOString().slice(0, 10); } function formatDateLabel(date) { return new Date(date).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); } function AnalyticsCharts({ routes }) { const chartIds = { runsPerWeek: 'runs-per-week-chart', distanceOverTime: 'distance-over-time-chart', paceTrends: 'pace-trends-chart', }; const chartData = useMemo(() => { const sorted = [...routes] .map((route) => ({ ...route, _date: getDateValue(route), _distanceKm: getDistanceKm(route), _pace: getPaceMinutesPerKm(route), })) .filter((route) => route._date) .sort((a, b) => a._date - b._date); const runsByWeek = new Map(); const distanceByDate = []; const paceByDate = []; sorted.forEach((route) => { const weekKey = formatWeekLabel(route._date); runsByWeek.set(weekKey, (runsByWeek.get(weekKey) || 0) + 1); distanceByDate.push({ label: formatDateLabel(route._date), value: route._distanceKm }); if (route._pace != null) { paceByDate.push({ label: formatDateLabel(route._date), value: Number(route._pace.toFixed(2)) }); } }); const weekEntries = Array.from(runsByWeek.entries()).sort((a, b) => a[0].localeCompare(b[0])); return { runsPerWeek: { labels: weekEntries.map(([week]) => week), values: weekEntries.map(([, count]) => count), }, distanceOverTime: { labels: distanceByDate.map((item) => item.label), values: distanceByDate.map((item) => item.value), }, paceTrends: { labels: paceByDate.map((item) => item.label), values: paceByDate.map((item) => item.value), }, }; }, [routes]); useEffect(() => { const renderIfReady = () => { const runsCanvas = document.getElementById(chartIds.runsPerWeek); const distanceCanvas = document.getElementById(chartIds.distanceOverTime); const paceCanvas = document.getElementById(chartIds.paceTrends); if (runsCanvas && chartData.runsPerWeek.labels.length > 0) { renderChart(runsCanvas, { type: 'bar', data: { labels: chartData.runsPerWeek.labels, datasets: [ { label: 'Runs', data: chartData.runsPerWeek.values, }, ], }, options: { responsive: true, maintainAspectRatio: false, }, }); } if (distanceCanvas && chartData.distanceOverTime.labels.length > 0) { renderChart(distanceCanvas, { type: 'line', data: { labels: chartData.distanceOverTime.labels, datasets: [ { label: 'Distance (km)', data: chartData.distanceOverTime.values, }, ], }, options: { responsive: true, maintainAspectRatio: false, }, }); } if (paceCanvas && chartData.paceTrends.labels.length > 0) { renderChart(paceCanvas, { type: 'line', data: { labels: chartData.paceTrends.labels, datasets: [ { label: 'Pace (min/km)', data: chartData.paceTrends.values, }, ], }, options: { responsive: true, maintainAspectRatio: false, }, }); } }; renderIfReady(); }, [chartData, chartIds.distanceOverTime, chartIds.paceTrends, chartIds.runsPerWeek]); const hasAnyData = routes.length > 0; return (

Run Analytics

Track your weekly activity, distance progression, and pace trends from your saved runs.

{!hasAnyData ? (
πŸ“ˆ

No run data yet

Save a few routes to see analytics charts here.

) : (

Runs per week

{chartData.runsPerWeek.labels.length > 0 ? ( ) : (
Not enough dated runs to build this chart.
)}

Distance over time

{chartData.distanceOverTime.labels.length > 0 ? ( ) : (
Not enough dated runs to build this chart.
)}

Pace trends

{chartData.paceTrends.labels.length > 0 ? ( ) : (
Add route pace/duration data to see pace trends.
)}
)}
); } function App() { const { user, loading, login, logout } = useAuth(); const [activeTab, setActiveTab] = useState('planner'); const [editingRoute, setEditingRoute] = useState(null); const [loadRoute, setLoadRoute] = useState(null); const [location, setLocation] = useState(null); const [locationStatus, setLocationStatus] = useState('idle'); const { items: savedRoutes } = useCollection('routes', { personal: true }); useEffect(() => { if (user) { setActiveTab('planner'); } }, [user]); useEffect(() => { if (!user) return; if (!navigator.geolocation) { setLocationStatus('unavailable'); return; } setLocationStatus('requesting'); navigator.geolocation.getCurrentPosition( (position) => { setLocation({ lat: position.coords.latitude, lng: position.coords.longitude, }); setLocationStatus('ready'); }, () => { setLocation(null); setLocationStatus('fallback'); }, { enableHighAccuracy: true, timeout: 10000, maximumAge: 600000, } ); }, [user]); const handleLoadRoute = (route) => { setLoadRoute(route); setActiveTab('planner'); }; if (loading) { return
Signing in...
; } if (!user) { return (
πŸƒ

RunRoute

Sign in with Google to save and manage your personal running routes.

); } const mapCenter = location ? [location.lat, location.lng] : undefined; return (
πŸƒ

RunRoute

Plan your perfect running route

Welcome, {user.name}
{activeTab === 'planner' && ( setLoadRoute(null)} initialCenter={mapCenter} userLocation={location} locationStatus={locationStatus} /> )} {activeTab === 'saved' && } {activeTab === 'analytics' && }
); } ReactDOM.createRoot(document.getElementById('root')).render();