feat: add frontend components for activities, leaderboard, teams, users, and workouts; include environment configuration

This commit is contained in:
2026-06-20 02:17:56 +00:00
parent 02569efb83
commit 9f2e85f60f
8 changed files with 333 additions and 114 deletions
@@ -0,0 +1,12 @@
# Vite environment variables for OctoFit Tracker
#
# Copy this file to .env.local and fill in your values.
# .env.local is gitignored and never committed.
#
# VITE_CODESPACE_NAME is required when running inside a GitHub Codespace.
# It is used to build the backend API base URL:
# https://${VITE_CODESPACE_NAME}-8000.app.github.dev/api
#
# If unset, the frontend falls back to http://localhost:8000/api.
VITE_CODESPACE_NAME=your-codespace-name-here
+31 -111
View File
@@ -1,121 +1,41 @@
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from './assets/vite.svg'
import heroImg from './assets/hero.png'
import './App.css'
import { BrowserRouter, Routes, Route, NavLink } from 'react-router-dom'
import Activities from './components/Activities'
import Leaderboard from './components/Leaderboard'
import Teams from './components/Teams'
import Users from './components/Users'
import Workouts from './components/Workouts'
function App() {
const [count, setCount] = useState(0)
return (
<>
<section id="center">
<div className="hero">
<img src={heroImg} className="base" width="170" height="179" alt="" />
<img src={reactLogo} className="framework" alt="React logo" />
<img src={viteLogo} className="vite" alt="Vite logo" />
<BrowserRouter>
<nav className="navbar navbar-expand-lg navbar-dark bg-dark">
<div className="container-fluid">
<span className="navbar-brand fw-bold">OctoFit Tracker</span>
<div className="navbar-nav">
<NavLink className={({ isActive }) => 'nav-link' + (isActive ? ' active' : '')} to="/activities">Activities</NavLink>
<NavLink className={({ isActive }) => 'nav-link' + (isActive ? ' active' : '')} to="/leaderboard">Leaderboard</NavLink>
<NavLink className={({ isActive }) => 'nav-link' + (isActive ? ' active' : '')} to="/teams">Teams</NavLink>
<NavLink className={({ isActive }) => 'nav-link' + (isActive ? ' active' : '')} to="/users">Users</NavLink>
<NavLink className={({ isActive }) => 'nav-link' + (isActive ? ' active' : '')} to="/workouts">Workouts</NavLink>
</div>
<div>
<h1>Get started</h1>
<p>
Edit <code>src/App.jsx</code> and save to test <code>HMR</code>
</p>
</div>
<button
type="button"
className="counter"
onClick={() => setCount((count) => count + 1)}
>
Count is {count}
</button>
</section>
<div className="ticks"></div>
<section id="next-steps">
<div id="docs">
<svg className="icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#documentation-icon"></use>
</svg>
<h2>Documentation</h2>
<p>Your questions, answered</p>
<ul>
<li>
<a href="https://vite.dev/" target="_blank">
<img className="logo" src={viteLogo} alt="" />
Explore Vite
</a>
</li>
<li>
<a href="https://react.dev/" target="_blank">
<img className="button-icon" src={reactLogo} alt="" />
Learn more
</a>
</li>
</ul>
</nav>
<div className="container mt-4">
<Routes>
<Route path="/activities" element={<Activities />} />
<Route path="/leaderboard" element={<Leaderboard />} />
<Route path="/teams" element={<Teams />} />
<Route path="/users" element={<Users />} />
<Route path="/workouts" element={<Workouts />} />
<Route path="/" element={
<div className="text-center mt-5">
<h1>Welcome to OctoFit Tracker</h1>
<p className="lead">Use the navigation above to explore activities, leaderboard, teams, users, and workouts.</p>
</div>
<div id="social">
<svg className="icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#social-icon"></use>
</svg>
<h2>Connect with us</h2>
<p>Join the Vite community</p>
<ul>
<li>
<a href="https://github.com/vitejs/vite" target="_blank">
<svg
className="button-icon"
role="presentation"
aria-hidden="true"
>
<use href="/icons.svg#github-icon"></use>
</svg>
GitHub
</a>
</li>
<li>
<a href="https://chat.vite.dev/" target="_blank">
<svg
className="button-icon"
role="presentation"
aria-hidden="true"
>
<use href="/icons.svg#discord-icon"></use>
</svg>
Discord
</a>
</li>
<li>
<a href="https://x.com/vite_js" target="_blank">
<svg
className="button-icon"
role="presentation"
aria-hidden="true"
>
<use href="/icons.svg#x-icon"></use>
</svg>
X.com
</a>
</li>
<li>
<a href="https://bsky.app/profile/vite.dev" target="_blank">
<svg
className="button-icon"
role="presentation"
aria-hidden="true"
>
<use href="/icons.svg#bluesky-icon"></use>
</svg>
Bluesky
</a>
</li>
</ul>
} />
</Routes>
</div>
</section>
<div className="ticks"></div>
<section id="spacer"></section>
</>
</BrowserRouter>
)
}
@@ -0,0 +1,60 @@
import { useEffect, useState } from 'react'
const codespaceName = import.meta.env.VITE_CODESPACE_NAME
const API_BASE = codespaceName
? `https://${codespaceName}-8000.app.github.dev/api`
: 'http://localhost:8000/api'
function Activities() {
const [activities, setActivities] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
fetch(`${API_BASE}/activities/`)
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return res.json()
})
.then((data) => {
setActivities(Array.isArray(data) ? data : (data.results ?? []))
setLoading(false)
})
.catch((err) => {
setError(err.message)
setLoading(false)
})
}, [])
if (loading) return <p>Loading activities</p>
if (error) return <p className="text-danger">Error: {error}</p>
return (
<div>
<h2>Activities</h2>
<table className="table table-striped">
<thead>
<tr>
<th>User</th>
<th>Type</th>
<th>Duration (min)</th>
<th>Date</th>
</tr>
</thead>
<tbody>
{activities.map((activity) => (
<tr key={activity._id ?? activity.id}>
<td>{activity.user}</td>
<td>{activity.activity_type}</td>
<td>{activity.duration}</td>
<td>{activity.date}</td>
</tr>
))}
</tbody>
</table>
{activities.length === 0 && <p>No activities found.</p>}
</div>
)
}
export default Activities
@@ -0,0 +1,58 @@
import { useEffect, useState } from 'react'
const codespaceName = import.meta.env.VITE_CODESPACE_NAME
const API_BASE = codespaceName
? `https://${codespaceName}-8000.app.github.dev/api`
: 'http://localhost:8000/api'
function Leaderboard() {
const [entries, setEntries] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
fetch(`${API_BASE}/leaderboard/`)
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return res.json()
})
.then((data) => {
setEntries(Array.isArray(data) ? data : (data.results ?? []))
setLoading(false)
})
.catch((err) => {
setError(err.message)
setLoading(false)
})
}, [])
if (loading) return <p>Loading leaderboard</p>
if (error) return <p className="text-danger">Error: {error}</p>
return (
<div>
<h2>Leaderboard</h2>
<table className="table table-striped">
<thead>
<tr>
<th>Rank</th>
<th>User</th>
<th>Score</th>
</tr>
</thead>
<tbody>
{entries.map((entry, index) => (
<tr key={entry._id ?? entry.id}>
<td>{index + 1}</td>
<td>{entry.user}</td>
<td>{entry.score}</td>
</tr>
))}
</tbody>
</table>
{entries.length === 0 && <p>No leaderboard entries found.</p>}
</div>
)
}
export default Leaderboard
@@ -0,0 +1,56 @@
import { useEffect, useState } from 'react'
const codespaceName = import.meta.env.VITE_CODESPACE_NAME
const API_BASE = codespaceName
? `https://${codespaceName}-8000.app.github.dev/api`
: 'http://localhost:8000/api'
function Teams() {
const [teams, setTeams] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
fetch(`${API_BASE}/teams/`)
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return res.json()
})
.then((data) => {
setTeams(Array.isArray(data) ? data : (data.results ?? []))
setLoading(false)
})
.catch((err) => {
setError(err.message)
setLoading(false)
})
}, [])
if (loading) return <p>Loading teams</p>
if (error) return <p className="text-danger">Error: {error}</p>
return (
<div>
<h2>Teams</h2>
<table className="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>Members</th>
</tr>
</thead>
<tbody>
{teams.map((team) => (
<tr key={team._id ?? team.id}>
<td>{team.name}</td>
<td>{Array.isArray(team.members) ? team.members.join(', ') : team.members}</td>
</tr>
))}
</tbody>
</table>
{teams.length === 0 && <p>No teams found.</p>}
</div>
)
}
export default Teams
@@ -0,0 +1,56 @@
import { useEffect, useState } from 'react'
const codespaceName = import.meta.env.VITE_CODESPACE_NAME
const API_BASE = codespaceName
? `https://${codespaceName}-8000.app.github.dev/api`
: 'http://localhost:8000/api'
function Users() {
const [users, setUsers] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
fetch(`${API_BASE}/users/`)
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return res.json()
})
.then((data) => {
setUsers(Array.isArray(data) ? data : (data.results ?? []))
setLoading(false)
})
.catch((err) => {
setError(err.message)
setLoading(false)
})
}, [])
if (loading) return <p>Loading users</p>
if (error) return <p className="text-danger">Error: {error}</p>
return (
<div>
<h2>Users</h2>
<table className="table table-striped">
<thead>
<tr>
<th>Username</th>
<th>Email</th>
</tr>
</thead>
<tbody>
{users.map((user) => (
<tr key={user._id ?? user.id}>
<td>{user.username}</td>
<td>{user.email}</td>
</tr>
))}
</tbody>
</table>
{users.length === 0 && <p>No users found.</p>}
</div>
)
}
export default Users
@@ -0,0 +1,56 @@
import { useEffect, useState } from 'react'
const codespaceName = import.meta.env.VITE_CODESPACE_NAME
const API_BASE = codespaceName
? `https://${codespaceName}-8000.app.github.dev/api`
: 'http://localhost:8000/api'
function Workouts() {
const [workouts, setWorkouts] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
fetch(`${API_BASE}/workouts/`)
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return res.json()
})
.then((data) => {
setWorkouts(Array.isArray(data) ? data : (data.results ?? []))
setLoading(false)
})
.catch((err) => {
setError(err.message)
setLoading(false)
})
}, [])
if (loading) return <p>Loading workouts</p>
if (error) return <p className="text-danger">Error: {error}</p>
return (
<div>
<h2>Workouts</h2>
<table className="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{workouts.map((workout) => (
<tr key={workout._id ?? workout.id}>
<td>{workout.name}</td>
<td>{workout.description}</td>
</tr>
))}
</tbody>
</table>
{workouts.length === 0 && <p>No workouts found.</p>}
</div>
)
}
export default Workouts
+1
View File
@@ -1,5 +1,6 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import 'bootstrap/dist/css/bootstrap.min.css'
import './index.css'
import App from './App.jsx'