Skip to main content
Glama
Dinesh-Satram

Health & Fitness Coach MCP

workout-tracker.tsxβ€’33.3 kB
"use client" import { useState, useEffect, useCallback } from "react" import { Dumbbell, Plus, Zap } from "lucide-react" import { Button } from "@/components/ui/button" import { Progress } from "@/components/ui/progress" import { cn } from "@/lib/utils" import { useFitnessData } from "@/hooks/useFitnessData" import { PlannedWorkout } from "@/lib/api" interface Exercise { id: string name: string sets: string reps: string weight?: string duration?: string completed: boolean type: "strength" | "cardio" } interface WorkoutTrackerProps { loggedWorkouts?: Array<{ id: string name: string type: string completed: boolean timestamp: string duration?: number }> currentPlan?: PlannedWorkout[] } export function WorkoutTracker({ loggedWorkouts = [], currentPlan }: WorkoutTrackerProps) { console.log('πŸ”„ WorkoutTracker received logged workouts:', loggedWorkouts) console.log('πŸ“‹ WorkoutTracker received current plan:', currentPlan) const { logNewEntry, refreshContext } = useFitnessData() // Helper function to check if an exercise is completed based on logged workouts const isExerciseCompleted = useCallback((exerciseName: string) => { const result = loggedWorkouts.some(workout => { const workoutName = workout.name.toLowerCase().trim() const exerciseName_lower = exerciseName.toLowerCase().trim() // Simplified matching logic let matches = false // Direct case-insensitive match if (workoutName === exerciseName_lower) { matches = true } // Handle push-ups variations else if (exerciseName_lower === 'push-ups') { matches = ['push-ups', 'pushups', 'push ups'].some(pattern => workoutName === pattern || workoutName.includes(pattern) || pattern.includes(workoutName) ) } // Handle squats variations else if (exerciseName_lower === 'squats') { matches = ['squats', 'squat'].some(pattern => workoutName === pattern || workoutName.includes(pattern) || pattern.includes(workoutName) ) } // Handle plank variations else if (exerciseName_lower === 'plank') { matches = ['plank', 'planks'].some(pattern => workoutName === pattern || workoutName.includes(pattern) || pattern.includes(workoutName) ) } // Handle lunges variations else if (exerciseName_lower === 'lunges') { matches = ['lunges', 'lunge'].some(pattern => workoutName === pattern || workoutName.includes(pattern) || pattern.includes(workoutName) ) } // Default substring matching else { matches = workoutName.includes(exerciseName_lower) || exerciseName_lower.includes(workoutName) } if (matches) { console.log(`βœ… ${exerciseName} matches workout: ${workout.name}`) } return matches }) console.log(`πŸ” ${exerciseName} completed status: ${result} (from ${loggedWorkouts.length} total workouts)`) return result }, [loggedWorkouts]) // Helper function to get actual duration from logged workouts (sum all matching sessions) const getActualDuration = useCallback((exerciseName: string) => { const matchingWorkouts = loggedWorkouts.filter(workout => { const workoutName = workout.name.toLowerCase().trim() const exerciseName_lower = exerciseName.toLowerCase().trim() // Use the same simplified matching logic let matches = false // Direct case-insensitive match if (workoutName === exerciseName_lower) { matches = true } // Handle push-ups variations else if (exerciseName_lower === 'push-ups') { matches = ['push-ups', 'pushups', 'push ups'].some(pattern => workoutName === pattern || workoutName.includes(pattern) || pattern.includes(workoutName) ) } // Handle squats variations else if (exerciseName_lower === 'squats') { matches = ['squats', 'squat'].some(pattern => workoutName === pattern || workoutName.includes(pattern) || pattern.includes(workoutName) ) } // Handle plank variations else if (exerciseName_lower === 'plank') { matches = ['plank', 'planks'].some(pattern => workoutName === pattern || workoutName.includes(pattern) || pattern.includes(workoutName) ) } // Handle lunges variations else if (exerciseName_lower === 'lunges') { matches = ['lunges', 'lunge'].some(pattern => workoutName === pattern || workoutName.includes(pattern) || pattern.includes(workoutName) ) } // Default substring matching else { matches = workoutName.includes(exerciseName_lower) || exerciseName_lower.includes(workoutName) } return matches }) // Sum all durations for this exercise type const totalDuration = matchingWorkouts.reduce((total, workout) => total + (workout.duration || 0), 0) console.log(`πŸ‹οΈ ${exerciseName}: Found ${matchingWorkouts.length} sessions, total duration: ${totalDuration} min`) return totalDuration }, [loggedWorkouts]) // Helper function to convert PlannedWorkout to Exercise format const convertPlanToExercises = useCallback((plan: PlannedWorkout[]): Exercise[] => { return plan.map(plannedWorkout => ({ id: plannedWorkout.id, name: plannedWorkout.name, sets: plannedWorkout.sets.toString(), reps: plannedWorkout.reps.toString(), weight: plannedWorkout.weight, duration: plannedWorkout.duration ? `${plannedWorkout.duration}${plannedWorkout.duration < 60 ? 's' : 'min'}` : undefined, completed: isExerciseCompleted(plannedWorkout.name), type: plannedWorkout.type })) }, [isExerciseCompleted]) // Helper function to determine if we're showing tomorrow's plan const isTomorrowsPlan = useCallback(() => { console.log('πŸ” Title detection - Checking if tomorrow\'s plan...') console.log('πŸ“Š LoggedWorkouts length:', loggedWorkouts.length) console.log('πŸ“Š CurrentPlan length:', currentPlan?.length || 0) // If we have no current plan, it's definitely today's workout (not tomorrow's) if (!currentPlan || currentPlan.length === 0) { console.log('❌ No current plan - showing today\'s workout') return false } // If we have logged workouts AND a current plan, check if plan is different if (loggedWorkouts.length > 0 && currentPlan.length > 0) { const loggedExerciseNames = loggedWorkouts.map(w => w.name.toLowerCase().trim()) const planExerciseNames = currentPlan.map(p => p.name.toLowerCase().trim()) console.log('πŸ“… Logged exercises:', loggedExerciseNames) console.log('πŸ“… Plan exercises:', planExerciseNames) // If plan has exercises NOT in logged workouts, it's tomorrow's plan const hasNewExercises = planExerciseNames.some(planEx => !loggedExerciseNames.some(loggedEx => planEx.includes(loggedEx) || loggedEx.includes(planEx) ) ) console.log('πŸ” Has new exercises?', hasNewExercises) if (hasNewExercises) { console.log('βœ… Detected tomorrow\'s plan!') return true } else { console.log('❌ Plan has same exercises as logged - showing today\'s workout') return false } } // If we have a plan but NO logged workouts, it could be either: // - Today's plan (before doing any workouts) // - Tomorrow's plan (generated for tomorrow) // Default to today's workout unless explicitly generated as tomorrow's plan if (currentPlan.length > 0 && loggedWorkouts.length === 0) { console.log('❌ Plan exists but no logged workouts - defaulting to today\'s workout') return false } console.log('❌ Default to today\'s workout') return false }, [loggedWorkouts, currentPlan]) // Determine the title and description const isTomorrow = isTomorrowsPlan() const workoutTitle = isTomorrow ? "Tomorrow's Workout" : "Today's Workout" const titleEmoji = isTomorrow ? "πŸŒ…" : "πŸ‹οΈ" console.log('πŸ“‹ Final workout title:', workoutTitle, titleEmoji) // Helper function to create exercises that preserve today's completed workouts const createMixedExercises = useCallback((): Exercise[] => { let exercisesToShow: Exercise[] = [] console.log('πŸ—οΈ Creating exercises list') console.log('πŸ“‹ Current plan provided:', !!currentPlan, currentPlan?.length || 0, 'exercises') console.log('πŸ’ͺ Logged workouts:', loggedWorkouts.length, loggedWorkouts.map(w => w.name)) // Strategy: Only show exercises that have been logged OR are in the current plan // Don't show any fallback exercises by default if (currentPlan && currentPlan.length > 0) { // If there's a plan, show plan exercises console.log('πŸ“‹ Using current plan exercises') exercisesToShow = currentPlan.map(planned => ({ id: planned.id, name: planned.name, sets: planned.sets.toString(), reps: planned.reps?.toString() || "", weight: planned.weight, duration: planned.duration ? `${planned.duration}s` : undefined, completed: isExerciseCompleted(planned.name), type: planned.type })) } // Add any logged exercises that aren't in the plan const loggedExerciseNames = new Set(exercisesToShow.map(e => e.name.toLowerCase())) loggedWorkouts.forEach((workout, index) => { if (!loggedExerciseNames.has(workout.name.toLowerCase())) { console.log('βž• Adding logged exercise not in plan:', workout.name) exercisesToShow.push({ id: `logged-${workout.id || index}`, name: workout.name, sets: "3", // Default values for logged exercises reps: "10", weight: undefined, duration: workout.duration ? `${workout.duration}min` : undefined, completed: true, // Logged exercises are always completed type: workout.type as "strength" | "cardio" || "strength" }) } }) // If no plan and no logged workouts, show empty list if (exercisesToShow.length === 0) { console.log('πŸ“‹ No plan and no logged workouts - showing empty list') return [] } console.log('βœ… Final exercises to show:', exercisesToShow.map(e => `${e.name}(${e.completed ? 'completed' : 'pending'})`)) return exercisesToShow }, [loggedWorkouts, currentPlan, isExerciseCompleted]) // Initialize exercises with mixed approach that preserves today's progress const [exercises, setExercises] = useState<Exercise[]>(() => { console.log('πŸš€ Initializing exercises') return createMixedExercises() }) // Update exercises when currentPlan or loggedWorkouts change useEffect(() => { console.log('πŸ”„ Updating exercises due to plan/workout changes') setExercises(createMixedExercises()) }, [createMixedExercises]) const [showAddForm, setShowAddForm] = useState(false) const [newExercise, setNewExercise] = useState({ name: "", sets: "", reps: "", weight: "", duration: "", type: "strength" as "strength" | "cardio", }) const toggleExercise = async (id: string) => { const exercise = exercises.find(ex => ex.id === id) if (!exercise) return const newCompletedState = !exercise.completed // Check if this exercise is already logged via + button const isAlreadyLogged = isExerciseCompleted(exercise.name) if (isAlreadyLogged && !newCompletedState) { // If trying to uncheck a logged exercise, just update local state but keep it completed // (since it was logged via + button, we don't want to fully uncheck it) return } // Update local state immediately for UI responsiveness setExercises( exercises.map((ex) => (ex.id === id ? { ...ex, completed: newCompletedState } : ex)), ) // If completing the exercise and it's not already logged, log it to the API if (newCompletedState && !isAlreadyLogged) { try { // Calculate duration based on exercise type and estimates let estimatedDuration = 5 // Default 5 minutes per exercise if (exercise.type === 'cardio' && exercise.duration) { // Extract duration from string like "30s" or "30min" const durationMatch = exercise.duration.match(/(\d+)/) if (durationMatch) { const number = parseInt(durationMatch[1]) if (exercise.duration.includes('min')) { estimatedDuration = number } else if (exercise.duration.includes('s')) { estimatedDuration = Math.max(1, Math.round(number / 60)) // Convert seconds to minutes } } } else if (exercise.sets && exercise.reps) { // Estimate time for strength exercises: sets * reps * 3 seconds + rest time const sets = parseInt(exercise.sets) || 3 const reps = parseInt(exercise.reps) || 10 estimatedDuration = Math.max(1, Math.round((sets * reps * 3 + sets * 60) / 60)) // 3 sec per rep + 60 sec rest per set } else if (exercise.duration) { // Use plan duration const durationMatch = exercise.duration.match(/(\d+)/) if (durationMatch) { const number = parseInt(durationMatch[1]) if (exercise.duration.includes('min')) { estimatedDuration = number } else if (exercise.duration.includes('s')) { estimatedDuration = Math.max(1, Math.round(number / 60)) } } } const result = await logNewEntry('workout', { workout: { type: exercise.type, name: exercise.name, duration: estimatedDuration, sets: parseInt(exercise.sets) || undefined, reps: parseInt(exercise.reps) || undefined, weight: exercise.weight || undefined, } }) if (result.success) { console.log(`βœ… Logged completed exercise: ${exercise.name} (${estimatedDuration} min)`) console.log('βœ… Progress should update automatically via logNewEntry') } else { console.error('❌ Failed to log exercise:', result.error) } } catch (error) { console.error('Failed to log exercise completion:', error) // Revert the state if API call failed setExercises( exercises.map((ex) => (ex.id === id ? { ...ex, completed: !newCompletedState } : ex)), ) } } } const handleAddExercise = () => { if (!newExercise.name || !newExercise.sets) return const exercise: Exercise = { id: Date.now().toString(), name: newExercise.name, sets: newExercise.sets, reps: newExercise.reps, weight: newExercise.weight || undefined, duration: newExercise.duration || undefined, completed: false, type: newExercise.type, } setExercises([...exercises, exercise]) setNewExercise({ name: "", sets: "", reps: "", weight: "", duration: "", type: "strength", }) setShowAddForm(false) } const completedExercises = exercises.filter((exercise) => exercise.completed) const progress = (completedExercises.length / exercises.length) * 100 // Calculate total time based on completed exercises and estimates for remaining let actualTime = 0 let estimatedTime = 0 exercises.forEach(exercise => { if (exercise.completed) { // Use actual logged duration for completed exercises if available const loggedDuration = getActualDuration(exercise.name) if (loggedDuration > 0) { actualTime += loggedDuration } else { // Fallback to plan duration if manually completed but not logged if (exercise.duration) { const durationMatch = exercise.duration.match(/(\d+)/) if (durationMatch) { const number = parseInt(durationMatch[1]) if (exercise.duration.includes('min')) { actualTime += number } else if (exercise.duration.includes('s')) { actualTime += Math.max(1, Math.round(number / 60)) } } } else { actualTime += 5 // Default 5 minutes } } } else { // Estimates for uncompleted exercises if (exercise.duration) { const durationMatch = exercise.duration.match(/(\d+)/) if (durationMatch) { const number = parseInt(durationMatch[1]) if (exercise.duration.includes('min')) { estimatedTime += number } else if (exercise.duration.includes('s')) { estimatedTime += Math.max(1, Math.round(number / 60)) } } } else { estimatedTime += 5 // Default 5 minutes per exercise } } }) const timeData = { actualTime, estimatedTime, totalTime: actualTime + estimatedTime } return ( <div className="bg-white/80 backdrop-blur-sm rounded-3xl shadow-lg border border-white/20 p-6"> {/* Header */} <div className="flex items-center justify-between mb-6"> <div className="flex items-center gap-3"> <div className="w-12 h-12 bg-gradient-to-br from-orange-400 to-red-500 rounded-2xl flex items-center justify-center"> <Dumbbell className="w-6 h-6 text-white" /> </div> <div> <h3 className="text-lg font-bold text-slate-800">{workoutTitle}</h3> <div className="flex items-center gap-2 mt-1"> <Progress value={progress} className="w-20 h-2" /> <span className="text-sm text-slate-600">{Math.round(progress)}%</span> </div> </div> </div> <Button size="sm" className="rounded-full bg-gradient-to-r from-orange-400 to-red-500 hover:from-orange-500 hover:to-red-600" onClick={() => setShowAddForm(!showAddForm)} > <Plus className="w-4 h-4" /> </Button> </div> {/* Add Exercise Form */} {showAddForm && ( <div className="mb-6 p-4 bg-gradient-to-r from-orange-50 to-red-50 rounded-2xl border border-orange-200/50 animate-in slide-in-from-top-2 duration-300"> <h4 className="font-semibold text-orange-800 mb-3 text-sm">Add New Exercise</h4> <div className="space-y-3"> <div className="grid grid-cols-2 gap-3"> <input type="text" placeholder="Exercise name" value={newExercise.name} onChange={(e) => setNewExercise({ ...newExercise, name: e.target.value })} className="px-3 py-2 rounded-xl border border-coral-200 bg-white/80 text-sm focus:outline-none focus:ring-2 focus:ring-coral-400" /> <select value={newExercise.type} onChange={(e) => setNewExercise({ ...newExercise, type: e.target.value as "strength" | "cardio" })} className="px-3 py-2 rounded-xl border border-coral-200 bg-white/80 text-sm focus:outline-none focus:ring-2 focus:ring-coral-400" > <option value="strength">Strength</option> <option value="cardio">Cardio</option> </select> </div> <div className="grid grid-cols-3 gap-3"> <input type="text" placeholder="Sets" value={newExercise.sets} onChange={(e) => setNewExercise({ ...newExercise, sets: e.target.value })} className="px-3 py-2 rounded-xl border border-coral-200 bg-white/80 text-sm focus:outline-none focus:ring-2 focus:ring-coral-400" /> <input type="text" placeholder="Reps" value={newExercise.reps} onChange={(e) => setNewExercise({ ...newExercise, reps: e.target.value })} className="px-3 py-2 rounded-xl border border-coral-200 bg-white/80 text-sm focus:outline-none focus:ring-2 focus:ring-coral-400" /> <input type="text" placeholder="Weight (optional)" value={newExercise.weight} onChange={(e) => setNewExercise({ ...newExercise, weight: e.target.value })} className="px-3 py-2 rounded-xl border border-coral-200 bg-white/80 text-sm focus:outline-none focus:ring-2 focus:ring-coral-400" /> </div> {newExercise.type === "cardio" && ( <input type="text" placeholder="Duration (e.g., 30min)" value={newExercise.duration} onChange={(e) => setNewExercise({ ...newExercise, duration: e.target.value })} className="w-full px-3 py-2 rounded-xl border border-coral-200 bg-white/80 text-sm focus:outline-none focus:ring-2 focus:ring-coral-400" /> )} <div className="flex gap-2"> <Button size="sm" onClick={handleAddExercise} className="flex-1 rounded-xl bg-gradient-to-r from-coral-400 to-red-500 hover:from-coral-500 hover:to-red-600" > Add Exercise </Button> <Button size="sm" variant="outline" onClick={() => setShowAddForm(false)} className="rounded-xl border-coral-200 text-coral-600 hover:bg-coral-50" > Cancel </Button> </div> </div> </div> )} {/* Exercises List */} {exercises.length === 0 ? ( // Empty state <div className="text-center py-8"> <div className="w-16 h-16 mx-auto mb-4 bg-orange-100 rounded-full flex items-center justify-center"> <Dumbbell className="w-8 h-8 text-orange-500" /> </div> <h4 className="text-lg font-semibold text-slate-700 mb-2">Ready to Start?</h4> <p className="text-sm text-slate-500 mb-4"> Log your first workout using the + button or chat interface </p> <div className="text-xs text-slate-400"> Try saying: "I did 20 push-ups for 10 minutes" </div> </div> ) : ( <> <div className="space-y-3 mb-6"> {exercises.map((exercise) => ( <button key={exercise.id} onClick={() => toggleExercise(exercise.id)} className={cn( "w-full p-4 rounded-2xl border-2 transition-all duration-200 text-left group", exercise.completed ? "bg-gradient-to-r from-green-50 to-emerald-50 border-green-200 shadow-sm" : "bg-gradient-to-r from-slate-50 to-gray-50 border-slate-200 hover:border-coral-300 hover:shadow-md hover:scale-[1.01]", )} > <div className="flex items-start justify-between"> <div className="flex items-start gap-3 flex-1"> <div className={cn( "w-4 h-4 rounded-full mt-0.5 transition-all duration-200 flex items-center justify-center", exercise.completed ? "bg-green-500 shadow-lg shadow-green-200" : "bg-slate-300 group-hover:bg-coral-400", )} > {exercise.completed && ( <svg className="w-2.5 h-2.5 text-white" fill="currentColor" viewBox="0 0 20 20"> <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" /> </svg> )} </div> <div className="flex-1 min-w-0"> <div className="flex items-center gap-2 mb-1"> <span className={cn( "text-xs font-semibold px-2 py-1 rounded-full", exercise.completed ? "bg-green-100 text-green-700" : exercise.type === "strength" ? "bg-coral-100 text-coral-700 group-hover:bg-coral-200" : "bg-blue-100 text-blue-700 group-hover:bg-blue-200", )} > {exercise.type === "strength" ? "STRENGTH" : "CARDIO"} </span> {exercise.type === "strength" && ( <div className="flex items-center gap-1 text-xs text-slate-500"> <Dumbbell className="w-3 h-3" /> </div> )} {exercise.type === "cardio" && ( <div className="flex items-center gap-1 text-xs text-slate-500"> <Zap className="w-3 h-3" /> </div> )} </div> <p className={cn( "font-medium text-sm leading-relaxed mb-2", exercise.completed ? "text-green-800" : "text-slate-700", )} > {exercise.name} </p> <div className="flex items-center gap-4 text-xs"> <span className={exercise.completed ? "text-green-600" : "text-slate-500"}> {exercise.sets} sets </span> {exercise.reps && ( <span className={exercise.completed ? "text-green-600" : "text-slate-500"}> {exercise.reps} reps </span> )} {exercise.weight && ( <span className={exercise.completed ? "text-green-600" : "text-slate-500"}> {exercise.weight} </span> )} {exercise.duration && ( <span className={exercise.completed ? "text-green-600" : "text-slate-500"}> {exercise.duration} </span> )} </div> </div> </div> </div> </button> ))} </div> {showAddForm && ( <div className="border-t border-slate-200 pt-4"> <div className="space-y-3"> <div className="flex gap-2"> <input type="text" placeholder="Exercise name" value={newExercise.name} onChange={(e) => setNewExercise(prev => ({ ...prev, name: e.target.value }))} className="flex-1 px-3 py-2 border border-slate-300 rounded-lg text-sm" /> <select value={newExercise.type} onChange={(e) => setNewExercise(prev => ({ ...prev, type: e.target.value as "strength" | "cardio" }))} className="px-3 py-2 border border-slate-300 rounded-lg text-sm" > <option value="strength">Strength</option> <option value="cardio">Cardio</option> </select> </div> <div className="flex gap-2"> <input type="text" placeholder="Sets" value={newExercise.sets} onChange={(e) => setNewExercise(prev => ({ ...prev, sets: e.target.value }))} className="w-16 px-3 py-2 border border-slate-300 rounded-lg text-sm" /> <input type="text" placeholder="Reps" value={newExercise.reps} onChange={(e) => setNewExercise(prev => ({ ...prev, reps: e.target.value }))} className="w-16 px-3 py-2 border border-slate-300 rounded-lg text-sm" /> <input type="text" placeholder="Weight" value={newExercise.weight} onChange={(e) => setNewExercise(prev => ({ ...prev, weight: e.target.value }))} className="flex-1 px-3 py-2 border border-slate-300 rounded-lg text-sm" /> <input type="text" placeholder="Duration" value={newExercise.duration} onChange={(e) => setNewExercise(prev => ({ ...prev, duration: e.target.value }))} className="flex-1 px-3 py-2 border border-slate-300 rounded-lg text-sm" /> </div> <div className="flex gap-2 justify-end"> <button onClick={() => setShowAddForm(false)} className="px-4 py-2 text-sm text-slate-600 hover:text-slate-800" > Cancel </button> <button onClick={handleAddExercise} className="px-4 py-2 bg-orange-500 text-white text-sm rounded-lg hover:bg-orange-600" > Add Exercise </button> </div> </div> </div> )} <div className="border-t border-slate-200 pt-4"> <div className="flex items-center justify-between text-sm"> <span className="text-slate-600"> Time: {timeData.actualTime > 0 && timeData.estimatedTime > 0 ? `${timeData.actualTime} + ${timeData.estimatedTime} min` : timeData.actualTime > 0 ? `${timeData.actualTime} min actual` : timeData.estimatedTime > 0 ? `${timeData.estimatedTime} min estimated` : '0 min' } </span> <button onClick={() => setShowAddForm(!showAddForm)} className="flex items-center gap-1 text-orange-500 hover:text-orange-600" > <Plus className="w-4 h-4" /> Add Exercise </button> </div> </div> </> )} {/* Workout Summary */} <div className="bg-gradient-to-r from-orange-50 to-red-50 rounded-2xl p-4 border border-orange-200/50"> <h4 className="font-semibold text-orange-800 mb-3 text-sm flex items-center gap-2"> <Zap className="w-4 h-4" /> Workout Progress </h4> <div className="grid grid-cols-2 gap-4"> <div> <div className="flex items-center justify-between mb-1"> <span className="text-xs text-orange-600">Exercises</span> <span className="text-xs text-orange-600"> {completedExercises.length}/{exercises.length} </span> </div> <div className="w-full bg-orange-200 rounded-full h-2"> <div className="bg-gradient-to-r from-orange-400 to-red-500 h-2 rounded-full transition-all duration-300" style={{ width: `${progress}%` }} ></div> </div> </div> <div> <div className="flex items-center justify-between mb-1"> <span className="text-xs text-orange-600">Time</span> <span className="text-xs text-orange-600"> {timeData.actualTime > 0 ? `${timeData.actualTime}` : '0'} {timeData.estimatedTime > 0 ? ` + ${timeData.estimatedTime}` : ''} min </span> </div> <div className="w-full bg-orange-200 rounded-full h-2"> <div className="bg-gradient-to-r from-orange-400 to-red-500 h-2 rounded-full transition-all duration-300" style={{ width: timeData.totalTime > 0 ? `${(timeData.actualTime / timeData.totalTime) * 100}%` : '0%' }} ></div> </div> </div> </div> </div> </div> ) }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Dinesh-Satram/fitness_coach_MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server