A clean, interactive web app to upload running metrics from CSV files and visualize performance across multiple runners. Track total miles, averages, and individual progress with real-time charts.
This is a Next.js + React dashboard that lets you:
- Upload CSV files with running data (date, person, miles)
- See aggregate metrics and charts across all runners
- Click to filter by individual runners
- Toggle person data on/off in the overall chart
- Load sample data to explore the UI quickly
Built with modern tools: TypeScript, Tailwind CSS, shadcn/ui components, and Recharts for visualization.
CSV Format
- We expect exactly 3 columns:
date,person,miles run - Dates are parsed as ISO strings (e.g.,
2025-01-01) - Miles are positive numbers; negative values are rejected
- Empty rows and missing required fields are skipped with error messages
State Management
- Using React Context (
RunDataContext) instead of Redux/Zustand for simplicity - Data stays in memory; no persistence layer (fresh start each session)
- Selected person filter resets when you clear data
UI/UX Choices
- Header stays sticky at the top for quick access to controls
- Metric cards are clickable to filter (felt more intuitive than separate filter UI)
- Top Performer card is interactive—click it to see that runner's data
- Chart toggles use small pill buttons (not checkboxes) for a modern feel
- Sample CSV lives in
/publicfor quick loading without a backend
Error Handling
- Parse errors surface inline, not in a modal—easier to fix and retry
- No toast notifications; errors appear below the upload card
- Invalid rows skip silently with a summary error message
- Node.js — v18 or later (tested with v20)
- npm — comes with Node
- No database or environment variables needed
npm installThis installs:
next(React framework)react&react-dom(UI library)papaparse(CSV parsing)recharts(charting)tailwindcss(styling)class-variance-authority&tailwind-merge(shadcn/ui utilities)
No .env file needed. The app runs entirely client-side.
Data comes from CSV uploads or the sample file at /public/sample.csv.
npm run devOpen http://localhost:3000 (or 3001 if 3000 is busy).
-
Load Sample Data
- Click the green "🎬 Load Sample" button
- You should see 2 metrics cards populate with Alice & Bob's data
- Charts appear below (Overall Miles per Day + individual runnerlines)
-
Click a Metric Card
- Click the "⭐ Top Performer" card → dashboard filters to that runner
- Click "🏃 Total Miles" → clears the filter, shows all runners again
-
Toggle Chart Lines
- Above the Overall chart, see small pill buttons for each runner (Alice, Bob, etc.)
- Click a pill to show/hide that runner's line on the chart
- Useful when too many overlapping runners clutter the view
-
Upload Your Own CSV
- Click "📁 Choose file" and pick any CSV with columns:
date,person,miles run - See results immediately or error messages if format is wrong
- Example format:
date,person,miles run 2025-01-15,Alice,5.2 2025-01-15,Bob,3.8 2025-01-16,Alice,4.1
- Click "📁 Choose file" and pick any CSV with columns:
-
Clear Data
- Click red "🗑️ Clear" button → metrics, charts, and selections reset
✅ What's Implemented
- CSV upload with real-time parsing and validation
- Metric cards: Total Miles, Average, Top Performer, Max
- Overall Miles per Day line chart (responsive, toggleable per person)
- Individual runner charts (click a runner, see their log)
- Sample CSV loader (one-click demo data)
- Sticky header with branding
- Dark/light card styling with gradients
- Fully responsive layout (mobile, tablet, desktop)
- TypeScript for type safety across components
- shadcn/ui components for polished buttons and cards
- No data persistence (refresh = data lost)
- No export to CSV (one-way upload only)
- Charts don't zoom or pan (Recharts limitation at this size)
- No multi-file upload (one at a time)
- No user accounts or saved sessions
- No date range filtering (shows all data at once)
🚀 Future Ideas
- Add date range picker to filter by week/month
- Export filtered results as CSV
- Compare two runners side-by-side
- Store data in localStorage for persistence
- Add drag-to-reorder chart lines
- Weekly/monthly aggregate tabs
- Dark mode toggle
components/
├── Header.tsx # Sticky nav with logo & settings button
├── CsvUploader.tsx # File input + error display
├── MetricsCards.tsx # 4 stat cards (total, avg, top, max)
├── OverallChart.tsx # Line chart with toggleable runner series
├── PersonChart.tsx # Individual runner's time series
├── PersonList.tsx # Runner selector buttons
└── ui/
├── button.tsx # shadcn Button component
└── card.tsx # shadcn Card + sub-components
lib/
├── csv/
│ └── parse.ts # CSV parsing + validation logic
├── metrics/
│ └── calc.ts # Math: totals, averages, aggregates
└── utils.ts # Helper: cn() for Tailwind merging
context/
└── RunDataContext.tsx # React Context: rows, selectedPerson state
app/
├── layout.tsx # Root layout
├── globals.css # Global Tailwind directives
├── page.tsx # Dashboard page component
└── page.module.css # (if any page-specific styles)
public/
└── sample.csv # Demo data (2 sample rows)
- CsvUploader → parses file → calls
setRows()via context - MetricsCards → reads
rowsfrom context → computes stats → displays cards - OverallChart → reads
rows→ builds per-person time series → user toggles visibility - PersonList → reads
rows+selectedPerson→ shows runner buttons - PersonChart → filters
rowsbyselectedPerson→ shows individual chart
No prop drilling; everything flows through RunDataContext.
parseCsv(file)→ Promise resolving to{ rows, errors }computeMetrics(rows)→{ total, avg, min, max }aggregateByDate(rows)→ array of{ date, miles }per dayuseRunData()→ returns context value or throws error if outside provider
- All buttons have visible focus rings (ring-2 ring-blue-500)
- Tab order follows visual flow: Header → Upload → Metrics → Charts → Runners
- Buttons are large enough to tap on mobile (min 44px)
- Form inputs have associated
<label>elements - Error messages use
role="alert"attributes (implied by error container) - Chart titles use
<h2>and<h3>tags, not divs
- Text on white: gray-900 (9:1 ratio, WCAG AAA)
- Text on colored cards: ensure 7:1+ ratio with gradients
- Error messages: red-800 on red-50 background (clear distinction)
- Base spacing uses Tailwind's 4px grid (gap-4 = 1rem)
- Metric card icons are large (text-2xl) and centered
- Chart labels are 12-14px, readable without zoom
- Line height on body text: 1.5 (comfortable reading)
- Metrics: 1 column on mobile, 2 on tablet, 4 on desktop
- Charts: full width, height adjusts for small screens
- Header nav hidden on mobile, shown on tablet+
- Upload card stacks vertically on small screens
- Button variants:
default(blue),destructive(red),outline(bordered),ghost(minimal) - Card styling: subtle borders, light shadows, no heavy gradients on text
- Icons via emoji (accessible, no alt text needed)
- All colors defined in Tailwind config
- Cards use
dark:variants for dark theme (future feature) - Text contrast tested against both light and dark backgrounds
Tested & working on:
- Chrome 90+
- Firefox 88+
- Safari 15+
- Edge 90+
Mobile: iOS Safari 15+, Chrome Android 90+
- Create in
components/(orcomponents/ui/for reusables) - Use TypeScript interfaces for props
- Import from
@/components/(alias works thanks to tsconfig)
Edit REQUIRED_HEADERS in lib/csv/parse.ts and update validation logic.
Edit PERSON_COLORS in components/OverallChart.tsx.
npm run build
npm startBuilds a static-optimized Next.js app ready for deployment.
"Port 3000 is in use"
- Next.js auto-switches to 3001. If you want a specific port:
npm run dev -- -p 3003
CSV Upload Shows No Errors But No Data Appears
- Check the CSV headers match exactly:
date,person,miles run(case-sensitive) - Ensure dates are valid ISO format or parseable by
new Date()
Chart Not Showing
- Make sure at least one valid row of data was loaded
- Check browser DevTools for console errors
Buttons Look Wrong
- Clear browser cache and hard-refresh (Ctrl+Shift+R on Windows, Cmd+Shift+R on Mac)
Built as a learning project. Feel free to fork, modify, and use however you like.