mirror of
https://github.com/sstent/AICyclingCoach.git
synced 2026-02-04 21:41:49 +00:00
sync
This commit is contained in:
181
frontend/pages/Dashboard.jsx
Normal file
181
frontend/pages/Dashboard.jsx
Normal file
@@ -0,0 +1,181 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import GarminSync from '../src/components/garmin/GarminSync';
|
||||
import WorkoutChart from '../src/components/analysis/WorkoutCharts';
|
||||
import PlanTimeline from '../src/components/plans/PlanTimeline';
|
||||
import { useAuth } from '../src/context/AuthContext';
|
||||
import LoadingSpinner from '../src/components/LoadingSpinner';
|
||||
|
||||
const Dashboard = () => {
|
||||
const { apiKey, loading: apiLoading } = useAuth();
|
||||
const isBuildTime = typeof window === 'undefined';
|
||||
const [recentWorkouts, setRecentWorkouts] = useState([]);
|
||||
const [currentPlan, setCurrentPlan] = useState(null);
|
||||
const [stats, setStats] = useState({ totalWorkouts: 0, totalDistance: 0 });
|
||||
const [healthStatus, setHealthStatus] = useState(null);
|
||||
const [localLoading, setLocalLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDashboardData = async () => {
|
||||
try {
|
||||
const [workoutsRes, planRes, statsRes, healthRes] = await Promise.all([
|
||||
fetch(`${process.env.REACT_APP_API_URL}/api/workouts?limit=3`, {
|
||||
headers: { 'X-API-Key': apiKey }
|
||||
}),
|
||||
fetch(`${process.env.REACT_APP_API_URL}/api/plans/active`, {
|
||||
headers: { 'X-API-Key': apiKey }
|
||||
}),
|
||||
fetch(`${process.env.REACT_APP_API_URL}/api/stats`, {
|
||||
headers: { 'X-API-Key': apiKey }
|
||||
}),
|
||||
fetch(`${process.env.REACT_APP_API_URL}/api/health`, {
|
||||
headers: { 'X-API-Key': apiKey }
|
||||
})
|
||||
]);
|
||||
|
||||
const errors = [];
|
||||
if (!workoutsRes.ok) errors.push('Failed to fetch workouts');
|
||||
if (!planRes.ok) errors.push('Failed to fetch plan');
|
||||
if (!statsRes.ok) errors.push('Failed to fetch stats');
|
||||
if (!healthRes.ok) errors.push('Failed to fetch health status');
|
||||
|
||||
if (errors.length > 0) throw new Error(errors.join(', '));
|
||||
|
||||
const [workoutsData, planData, statsData, healthData] = await Promise.all([
|
||||
workoutsRes.json(),
|
||||
planRes.json(),
|
||||
statsRes.json(),
|
||||
healthRes.json()
|
||||
]);
|
||||
|
||||
setRecentWorkouts(workoutsData.workouts || []);
|
||||
setCurrentPlan(planData);
|
||||
setStats(statsData.workouts || { totalWorkouts: 0, totalDistance: 0 });
|
||||
setHealthStatus(healthData);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLocalLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchDashboardData();
|
||||
}, [apiKey]);
|
||||
|
||||
if (isBuildTime) {
|
||||
return (
|
||||
<div className="p-6 max-w-7xl mx-auto">
|
||||
<h1 className="text-3xl font-bold">Training Dashboard</h1>
|
||||
<div className="bg-white p-6 rounded-lg shadow-md">
|
||||
<p className="text-gray-600">Loading dashboard data...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (localLoading || apiLoading) return <LoadingSpinner />;
|
||||
if (error) return <div className="p-6 text-red-500">{error}</div>;
|
||||
|
||||
// Calculate total distance in km
|
||||
const totalDistanceKm = (stats.totalDistance / 1000).toFixed(0);
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-7xl mx-auto space-y-8">
|
||||
<h1 className="text-3xl font-bold">Training Dashboard</h1>
|
||||
|
||||
<div className="mb-8">
|
||||
<GarminSync apiKey={apiKey} />
|
||||
</div>
|
||||
|
||||
{/* Stats Summary Cards */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="bg-white p-4 rounded-lg shadow-md">
|
||||
<h3 className="text-sm font-medium text-gray-600">Total Workouts</h3>
|
||||
<p className="text-2xl font-bold">{stats.totalWorkouts}</p>
|
||||
</div>
|
||||
<div className="bg-white p-4 rounded-lg shadow-md">
|
||||
<h3 className="text-sm font-medium text-gray-600">Total Distance</h3>
|
||||
<p className="text-2xl font-bold">{totalDistanceKm} km</p>
|
||||
</div>
|
||||
<div className="bg-white p-4 rounded-lg shadow-md">
|
||||
<h3 className="text-sm font-medium text-gray-600">Current Plan</h3>
|
||||
<p className="text-2xl font-bold">
|
||||
{currentPlan ? `v${currentPlan.version}` : 'None'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white p-4 rounded-lg shadow-md">
|
||||
<h3 className="text-sm font-medium text-gray-600">System Status</h3>
|
||||
<div className="flex items-center">
|
||||
<div className={`w-3 h-3 rounded-full mr-2 ${
|
||||
healthStatus?.status === 'healthy' ? 'bg-green-500' : 'bg-red-500'
|
||||
}`}></div>
|
||||
<span className="text-xl font-bold capitalize">
|
||||
{healthStatus?.status || 'unknown'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4 lg:gap-6">
|
||||
<div className="sm:col-span-2 space-y-3 sm:space-y-4 lg:space-y-6">
|
||||
<div className="bg-white p-4 sm:p-6 rounded-lg shadow-md">
|
||||
<h2 className="text-lg sm:text-xl font-semibold mb-3 sm:mb-4">Performance Metrics</h2>
|
||||
<WorkoutChart workouts={recentWorkouts} />
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-4 sm:p-6 rounded-lg shadow-md">
|
||||
<h2 className="text-lg sm:text-xl font-semibold mb-3 sm:mb-4">Recent Activities</h2>
|
||||
{recentWorkouts.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{recentWorkouts.map(workout => (
|
||||
<div key={workout.id} className="p-3 sm:p-4 border rounded-lg hover:bg-gray-50">
|
||||
<div className="flex justify-between items-center gap-2">
|
||||
<div>
|
||||
<h3 className="text-sm sm:text-base font-medium">{new Date(workout.start_time).toLocaleDateString()}</h3>
|
||||
<p className="text-xs sm:text-sm text-gray-600">{workout.activity_type}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm sm:text-base font-medium">{(workout.distance_m / 1000).toFixed(1)} km</p>
|
||||
<p className="text-xs sm:text-sm text-gray-600">{Math.round(workout.duration_seconds / 60)} mins</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-gray-500 text-center py-4">No recent activities found</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white p-4 sm:p-6 rounded-lg shadow-md">
|
||||
<h2 className="text-lg sm:text-xl font-semibold mb-3 sm:mb-4">Current Plan</h2>
|
||||
{currentPlan ? (
|
||||
<PlanTimeline plan={currentPlan} />
|
||||
) : (
|
||||
<div className="text-gray-500 text-center py-4">No active training plan</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-4 sm:p-6 rounded-lg shadow-md">
|
||||
<h2 className="text-lg sm:text-xl font-semibold mb-3 sm:mb-4">Upcoming Workouts</h2>
|
||||
{currentPlan?.jsonb_plan.weeks[0]?.workouts.map((workout, index) => (
|
||||
<div key={index} className="p-2 sm:p-3 border-b last:border-b-0">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="capitalize">{workout.day}</span>
|
||||
<span className="text-xs sm:text-sm bg-blue-100 text-blue-800 px-2 py-1 rounded">
|
||||
{workout.type.replace('_', ' ')}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs sm:text-sm text-gray-600 mt-1">{workout.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
38
frontend/pages/PlanDetails.jsx
Normal file
38
frontend/pages/PlanDetails.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import PlanTimeline from '../src/components/PlanTimeline'
|
||||
|
||||
const PlanDetails = () => {
|
||||
const router = useRouter()
|
||||
const { planId } = router.query
|
||||
|
||||
// If the planId is not available yet (still loading), show a loading state
|
||||
if (!planId) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-4 md:p-6">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="p-4 space-y-4">
|
||||
<div className="animate-pulse space-y-4">
|
||||
<div className="h-6 bg-gray-200 rounded w-1/4"></div>
|
||||
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
|
||||
<div className="space-y-2">
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<div key={i} className="h-12 bg-gray-100 rounded-lg"></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-4 md:p-6">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<PlanTimeline planId={planId} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PlanDetails
|
||||
85
frontend/pages/PlanGeneration.jsx
Normal file
85
frontend/pages/PlanGeneration.jsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useAuth } from '../src/context/AuthContext';
|
||||
import GoalSelector from '../src/components/plans/GoalSelector';
|
||||
import PlanParameters from '../src/components/plans/PlanParameters';
|
||||
import { generatePlan } from '../src/services/planService';
|
||||
import ProgressTracker from '../src/components/ui/ProgressTracker';
|
||||
|
||||
const PlanGeneration = () => {
|
||||
const { apiKey } = useAuth();
|
||||
const router = useRouter();
|
||||
const [step, setStep] = useState(1);
|
||||
const [goals, setGoals] = useState([]);
|
||||
const [rules, setRules] = useState([]);
|
||||
const [params, setParams] = useState({
|
||||
duration: 4,
|
||||
weeklyHours: 8,
|
||||
availableDays: []
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleGenerate = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const plan = await generatePlan(apiKey, {
|
||||
goals,
|
||||
ruleIds: rules,
|
||||
...params
|
||||
});
|
||||
router.push(`/plans/${plan.id}/preview`);
|
||||
} catch (err) {
|
||||
setError('Failed to generate plan. Please try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
<ProgressTracker currentStep={step} totalSteps={3} />
|
||||
|
||||
{step === 1 && (
|
||||
<GoalSelector
|
||||
goals={goals}
|
||||
onSelect={setGoals}
|
||||
onNext={() => setStep(2)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{step === 2 && (
|
||||
<PlanParameters
|
||||
values={params}
|
||||
onChange={setParams}
|
||||
onBack={() => setStep(1)}
|
||||
onNext={() => setStep(3)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{step === 3 && (
|
||||
<div className="bg-white p-6 rounded-lg shadow-md">
|
||||
<h2 className="text-2xl font-bold mb-4">Review and Generate</h2>
|
||||
<div className="mb-6">
|
||||
<h3 className="font-semibold mb-2">Selected Goals:</h3>
|
||||
<ul className="list-disc pl-5">
|
||||
{goals.map((goal, index) => (
|
||||
<li key={index}>{goal}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
disabled={loading}
|
||||
className="bg-blue-600 text-white px-6 py-2 rounded-md hover:bg-blue-700 disabled:bg-gray-400"
|
||||
>
|
||||
{loading ? 'Generating...' : 'Generate Plan'}
|
||||
</button>
|
||||
{error && <p className="text-red-500 mt-2">{error}</p>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlanGeneration;
|
||||
10
frontend/pages/Plans.jsx
Normal file
10
frontend/pages/Plans.jsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
|
||||
const Plans = () => (
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl font-bold mb-4">Training Plans</h1>
|
||||
<p className="text-gray-600">Training plans page under development</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Plans;
|
||||
75
frontend/pages/RoutesPage.jsx
Normal file
75
frontend/pages/RoutesPage.jsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useAuth } from '../src/context/AuthContext';
|
||||
import FileUpload from '../src/components/routes/FileUpload';
|
||||
import RouteList from '../src/components/routes/RouteList';
|
||||
import RouteFilter from '../src/components/routes/RouteFilter';
|
||||
import LoadingSpinner from '../src/components/LoadingSpinner';
|
||||
|
||||
const RoutesPage = () => {
|
||||
const { apiKey } = useAuth();
|
||||
const [routes, setRoutes] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [filters, setFilters] = useState({
|
||||
searchQuery: '',
|
||||
minDistance: 0,
|
||||
maxDistance: 500,
|
||||
difficulty: 'all',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchRoutes = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/routes', {
|
||||
headers: { 'X-API-Key': apiKey }
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to fetch routes');
|
||||
|
||||
const data = await response.json();
|
||||
setRoutes(data.routes);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchRoutes();
|
||||
}, [apiKey]);
|
||||
|
||||
const filteredRoutes = routes.filter(route => {
|
||||
const matchesSearch = route.name.toLowerCase().includes(filters.searchQuery.toLowerCase());
|
||||
const matchesDistance = route.distance >= filters.minDistance &&
|
||||
route.distance <= filters.maxDistance;
|
||||
const matchesDifficulty = filters.difficulty === 'all' ||
|
||||
route.difficulty === filters.difficulty;
|
||||
return matchesSearch && matchesDistance && matchesDifficulty;
|
||||
});
|
||||
|
||||
const handleUploadSuccess = (newRoute) => {
|
||||
setRoutes(prev => [...prev, newRoute]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-7xl mx-auto">
|
||||
<h1 className="text-3xl font-bold mb-8">Routes</h1>
|
||||
<div className="space-y-8">
|
||||
<div className="bg-white p-6 rounded-lg shadow-md">
|
||||
<RouteFilter filters={filters} onFilterChange={setFilters} />
|
||||
<FileUpload onUploadSuccess={handleUploadSuccess} />
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<LoadingSpinner />
|
||||
) : error ? (
|
||||
<div className="text-red-600 bg-red-50 p-4 rounded-md">{error}</div>
|
||||
) : (
|
||||
<RouteList routes={filteredRoutes} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoutesPage;
|
||||
10
frontend/pages/Rules.jsx
Normal file
10
frontend/pages/Rules.jsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
|
||||
const Rules = () => (
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl font-bold mb-4">Training Rules</h1>
|
||||
<p className="text-gray-600">Training rules page under development</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Rules;
|
||||
10
frontend/pages/Workouts.jsx
Normal file
10
frontend/pages/Workouts.jsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
|
||||
const Workouts = () => (
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl font-bold mb-4">Workouts</h1>
|
||||
<p className="text-gray-600">Workouts page under development</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Workouts;
|
||||
11
frontend/pages/_app.js
Normal file
11
frontend/pages/_app.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { AuthProvider } from '../src/context/AuthContext';
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<Component {...pageProps} />
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default MyApp;
|
||||
13
frontend/pages/index.js
Normal file
13
frontend/pages/index.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
export default function Home() {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
// Redirect to dashboard
|
||||
router.push('/dashboard');
|
||||
}, [router]);
|
||||
|
||||
return null; // or a loading spinner
|
||||
}
|
||||
@@ -1,80 +1,10 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import PlanTimeline from '../components/plans/PlanTimeline';
|
||||
import React from 'react';
|
||||
|
||||
const Plans = () => {
|
||||
const { apiKey } = useAuth();
|
||||
const [plans, setPlans] = useState([]);
|
||||
const [selectedPlan, setSelectedPlan] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const isBuildTime = typeof window === 'undefined';
|
||||
|
||||
useEffect(() => {
|
||||
if (isBuildTime) return;
|
||||
const fetchPlans = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/plans', {
|
||||
headers: { 'X-API-Key': apiKey }
|
||||
});
|
||||
setPlans(response.data);
|
||||
if (response.data.length > 0) {
|
||||
setSelectedPlan(response.data[0].id);
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Failed to load training plans');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPlans();
|
||||
}, [apiKey]);
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
return (
|
||||
<div className="p-6 max-w-7xl mx-auto">
|
||||
<h1 className="text-3xl font-bold mb-8">Training Plans</h1>
|
||||
<div className="bg-white p-6 rounded-lg shadow-md">
|
||||
<p className="text-gray-600">Loading training plans...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) return <div className="p-6 text-center">Loading plans...</div>;
|
||||
if (error) return <div className="p-6 text-red-600">{error}</div>;
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-7xl mx-auto">
|
||||
<h1 className="text-3xl font-bold mb-8">Training Plans</h1>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="space-y-4">
|
||||
{plans.map(plan => (
|
||||
<div
|
||||
key={plan.id}
|
||||
onClick={() => setSelectedPlan(plan.id)}
|
||||
className={`p-4 bg-white rounded-lg shadow-md cursor-pointer ${
|
||||
selectedPlan === plan.id ? 'ring-2 ring-blue-500' : ''
|
||||
}`}
|
||||
>
|
||||
<h3 className="font-medium">Plan v{plan.version}</h3>
|
||||
<p className="text-sm text-gray-600">
|
||||
Created {new Date(plan.created_at).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-2">
|
||||
{selectedPlan && <PlanTimeline planId={selectedPlan} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const Plans = () => (
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl font-bold mb-4">Training Plans</h1>
|
||||
<p className="text-gray-600">Training plans page under development</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Plans;
|
||||
@@ -1,85 +1,10 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import RuleEditor from '../components/rules/RuleEditor';
|
||||
import RulePreview from '../components/rules/RulePreview';
|
||||
import RulesList from '../components/rules/RulesList';
|
||||
import { getRuleSets, createRuleSet, parseRule } from '../services/ruleService';
|
||||
import React from 'react';
|
||||
|
||||
const RulesPage = () => {
|
||||
const [ruleText, setRuleText] = useState('');
|
||||
const [parsedRules, setParsedRules] = useState(null);
|
||||
const [ruleSets, setRuleSets] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const Rules = () => (
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl font-bold mb-4">Training Rules</h1>
|
||||
<p className="text-gray-600">Training rules page under development</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Load initial rule sets
|
||||
useEffect(() => {
|
||||
const loadRuleSets = async () => {
|
||||
try {
|
||||
const { data } = await getRuleSets();
|
||||
setRuleSets(data);
|
||||
} catch (err) {
|
||||
setError('Failed to load rule sets');
|
||||
}
|
||||
};
|
||||
loadRuleSets();
|
||||
}, []);
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await createRuleSet({
|
||||
naturalLanguage: ruleText,
|
||||
jsonRules: parsedRules
|
||||
});
|
||||
setRuleText('');
|
||||
setParsedRules(null);
|
||||
// Refresh rule sets list
|
||||
const { data } = await getRuleSets();
|
||||
setRuleSets(data);
|
||||
} catch (err) {
|
||||
setError('Failed to save rule set');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-7xl mx-auto">
|
||||
<h1 className="text-3xl font-bold mb-6">Training Rules Management</h1>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-6 mb-8">
|
||||
<div className="flex-1">
|
||||
<RuleEditor
|
||||
value={ruleText}
|
||||
onChange={setRuleText}
|
||||
onParse={setParsedRules}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<RulePreview
|
||||
rules={parsedRules}
|
||||
onSave={handleSave}
|
||||
isSaving={isLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RulesList
|
||||
ruleSets={ruleSets}
|
||||
onSelect={(set) => {
|
||||
setRuleText(set.naturalLanguage);
|
||||
setParsedRules(set.jsonRules);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RulesPage;
|
||||
export default Rules;
|
||||
@@ -1,81 +1,10 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import axios from 'axios';
|
||||
import WorkoutAnalysis from '../components/analysis/WorkoutAnalysis';
|
||||
import React from 'react';
|
||||
|
||||
const Workouts = () => {
|
||||
const { apiKey } = useAuth();
|
||||
const [workouts, setWorkouts] = useState([]);
|
||||
const [selectedWorkout, setSelectedWorkout] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const isBuildTime = typeof window === 'undefined';
|
||||
|
||||
useEffect(() => {
|
||||
if (isBuildTime) return;
|
||||
const fetchWorkouts = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/workouts', {
|
||||
headers: { 'X-API-Key': apiKey }
|
||||
});
|
||||
setWorkouts(response.data.workouts);
|
||||
} catch (err) {
|
||||
setError('Failed to load workouts');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchWorkouts();
|
||||
}, [apiKey]);
|
||||
|
||||
if (isBuildTime) {
|
||||
return (
|
||||
<div className="p-6 max-w-7xl mx-auto">
|
||||
<h1 className="text-3xl font-bold mb-8">Workouts</h1>
|
||||
<div className="bg-white p-6 rounded-lg shadow-md">
|
||||
<p className="text-gray-600">Loading workout data...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) return <div className="p-6 text-center">Loading workouts...</div>;
|
||||
if (error) return <div className="p-6 text-red-600">{error}</div>;
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-7xl mx-auto">
|
||||
<h1 className="text-3xl font-bold mb-8">Workouts</h1>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div className="space-y-4">
|
||||
{workouts.map(workout => (
|
||||
<div
|
||||
key={workout.id}
|
||||
onClick={() => setSelectedWorkout(workout)}
|
||||
className={`p-4 bg-white rounded-lg shadow-md cursor-pointer ${
|
||||
selectedWorkout?.id === workout.id ? 'ring-2 ring-blue-500' : ''
|
||||
}`}
|
||||
>
|
||||
<h3 className="font-medium">
|
||||
{new Date(workout.start_time).toLocaleDateString()} - {workout.activity_type}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600">
|
||||
Duration: {Math.round(workout.duration_seconds / 60)}min
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{selectedWorkout && (
|
||||
<div className="bg-white p-6 rounded-lg shadow-md">
|
||||
<WorkoutAnalysis workoutId={selectedWorkout.id} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const Workouts = () => (
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl font-bold mb-4">Workouts</h1>
|
||||
<p className="text-gray-600">Workouts page under development</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Workouts;
|
||||
@@ -1,55 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import Navigation from '../components/Navigation';
|
||||
|
||||
export default function Home() {
|
||||
const [healthStatus, setHealthStatus] = useState<string>('checking...');
|
||||
|
||||
useEffect(() => {
|
||||
const checkBackendHealth = async () => {
|
||||
try {
|
||||
// Use the API URL from environment variables
|
||||
const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8000';
|
||||
const response = await fetch(`${apiUrl}/health`);
|
||||
const data = await response.json();
|
||||
setHealthStatus(data.status);
|
||||
} catch (error) {
|
||||
setHealthStatus('unavailable');
|
||||
console.error('Error checking backend health:', error);
|
||||
}
|
||||
};
|
||||
|
||||
checkBackendHealth();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Navigation />
|
||||
<div className="max-w-2xl mx-auto p-8">
|
||||
<div className="bg-white rounded-lg shadow-md p-8">
|
||||
<h1 className="text-3xl font-bold text-center text-gray-800 mb-6">
|
||||
Welcome to AI Cycling Coach
|
||||
</h1>
|
||||
<p className="text-lg text-gray-600 mb-8 text-center">
|
||||
Your AI-powered training companion for cyclists
|
||||
</p>
|
||||
|
||||
<div className="bg-blue-50 p-4 rounded-lg border border-blue-200">
|
||||
<h2 className="text-lg font-semibold text-blue-800 mb-2">
|
||||
System Status
|
||||
</h2>
|
||||
<div className="flex items-center">
|
||||
<div className={`h-3 w-3 rounded-full mr-2 ${healthStatus === 'healthy' ? 'bg-green-500' : 'bg-red-500'}`}></div>
|
||||
<span className="text-gray-700">
|
||||
Backend service: {healthStatus}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="mt-8 text-center text-gray-500">
|
||||
Development in progress - more features coming soon!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user