diff --git a/backend/src/routes/exercises.js b/backend/src/routes/exercises.js new file mode 100644 index 0000000..73e0866 --- /dev/null +++ b/backend/src/routes/exercises.js @@ -0,0 +1,173 @@ +const express = require('express'); +const pool = require('../db/pool'); +const router = express.Router(); + +// Validation helper +const validateExercise = (data) => { + const errors = []; + if (!data.name || typeof data.name !== 'string' || !data.name.trim()) { + errors.push('name is required and must be non-empty'); + } + if (data.difficulty && !['beginner', 'intermediate', 'advanced'].includes(data.difficulty)) { + errors.push('difficulty must be beginner, intermediate, or advanced'); + } + if (data.muscle_groups && !Array.isArray(data.muscle_groups)) { + errors.push('muscle_groups must be an array'); + } + if (data.equipment_needed && !Array.isArray(data.equipment_needed)) { + errors.push('equipment_needed must be an array'); + } + return errors; +}; + +// CREATE - Add new exercise +router.post('/', async (req, res) => { + try { + const { name, description, instructions, muscle_groups, difficulty, equipment_needed, video_url, created_by } = req.body; + + const errors = validateExercise({ name, difficulty, muscle_groups, equipment_needed }); + if (errors.length > 0) { + return res.status(400).json({ error: 'Validation failed', details: errors }); + } + + const query = ` + INSERT INTO exercises (name, description, instructions, muscle_groups, difficulty, equipment_needed, video_url, created_by) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + RETURNING * + `; + + const result = await pool.query(query, [ + name.trim(), + description || null, + instructions || null, + muscle_groups || [], + difficulty || 'intermediate', + equipment_needed || [], + video_url || null, + created_by || 'system' + ]); + + res.status(201).json(result.rows[0]); + } catch (err) { + if (err.code === '23505') { + return res.status(409).json({ error: 'Exercise name already exists' }); + } + console.error('Error creating exercise:', err); + res.status(500).json({ error: 'Failed to create exercise' }); + } +}); + +// READ - Get all exercises with search/filter +router.get('/', async (req, res) => { + try { + const { search, difficulty, muscle_group, limit = 50, offset = 0 } = req.query; + + let query = 'SELECT * FROM exercises WHERE 1=1'; + const params = []; + let paramCount = 1; + + if (search) { + query += ` AND (name ILIKE $${paramCount} OR description ILIKE $${paramCount})`; + params.push(`%${search}%`); + paramCount++; + } + + if (difficulty) { + query += ` AND difficulty = $${paramCount}`; + params.push(difficulty); + paramCount++; + } + + if (muscle_group) { + query += ` AND $${paramCount} = ANY(muscle_groups)`; + params.push(muscle_group); + paramCount++; + } + + query += ` ORDER BY name ASC LIMIT $${paramCount} OFFSET $${paramCount + 1}`; + params.push(parseInt(limit), parseInt(offset)); + + const result = await pool.query(query, params); + res.json(result.rows); + } catch (err) { + console.error('Error fetching exercises:', err); + res.status(500).json({ error: 'Failed to fetch exercises' }); + } +}); + +// READ - Get single exercise +router.get('/:id', async (req, res) => { + try { + const result = await pool.query('SELECT * FROM exercises WHERE id = $1', [req.params.id]); + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Exercise not found' }); + } + + res.json(result.rows[0]); + } catch (err) { + console.error('Error fetching exercise:', err); + res.status(500).json({ error: 'Failed to fetch exercise' }); + } +}); + +// UPDATE - Modify exercise +router.put('/:id', async (req, res) => { + try { + const { name, description, instructions, muscle_groups, difficulty, equipment_needed, video_url } = req.body; + + const errors = validateExercise({ name, difficulty, muscle_groups, equipment_needed }); + if (errors.length > 0) { + return res.status(400).json({ error: 'Validation failed', details: errors }); + } + + const query = ` + UPDATE exercises + SET name = $1, description = $2, instructions = $3, muscle_groups = $4, + difficulty = $5, equipment_needed = $6, video_url = $7, updated_at = CURRENT_TIMESTAMP + WHERE id = $8 + RETURNING * + `; + + const result = await pool.query(query, [ + name.trim(), + description || null, + instructions || null, + muscle_groups || [], + difficulty || 'intermediate', + equipment_needed || [], + video_url || null, + req.params.id + ]); + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Exercise not found' }); + } + + res.json(result.rows[0]); + } catch (err) { + if (err.code === '23505') { + return res.status(409).json({ error: 'Exercise name already exists' }); + } + console.error('Error updating exercise:', err); + res.status(500).json({ error: 'Failed to update exercise' }); + } +}); + +// DELETE - Remove exercise +router.delete('/:id', async (req, res) => { + try { + const result = await pool.query('DELETE FROM exercises WHERE id = $1 RETURNING *', [req.params.id]); + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Exercise not found' }); + } + + res.json({ message: 'Exercise deleted', id: req.params.id }); + } catch (err) { + console.error('Error deleting exercise:', err); + res.status(500).json({ error: 'Failed to delete exercise' }); + } +}); + +module.exports = router; diff --git a/db/migrations/005_create_exercises_table.sql b/db/migrations/005_create_exercises_table.sql new file mode 100644 index 0000000..d2995f9 --- /dev/null +++ b/db/migrations/005_create_exercises_table.sql @@ -0,0 +1,18 @@ +-- Create exercises table for exercise encyclopedia +CREATE TABLE IF NOT EXISTS exercises ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL UNIQUE, + description TEXT, + instructions TEXT, + muscle_groups TEXT[] DEFAULT ARRAY[]::text[], + difficulty VARCHAR(20) DEFAULT 'intermediate' CHECK (difficulty IN ('beginner', 'intermediate', 'advanced')), + equipment_needed TEXT[] DEFAULT ARRAY[]::text[], + video_url VARCHAR(255), + created_by VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_exercises_name ON exercises(name); +CREATE INDEX idx_exercises_difficulty ON exercises(difficulty); +CREATE INDEX idx_exercises_muscle_groups ON exercises USING GIN(muscle_groups);