feature/05-exercise-encyclopedia #4

Merged
sphinxen merged 28 commits from feature/05-exercise-encyclopedia into main 2026-03-06 12:29:20 +01:00
2 changed files with 191 additions and 0 deletions
Showing only changes of commit 994cc9e984 - Show all commits
+173
View File
@@ -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;
@@ -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);