Refactor: separera user_measurements och user_strength tabeller
- Ny databasstruktur för historik/progress tracking - Nya endpoints: POST/GET measurements och strength - Onboarding sparar till rätt tabeller - Beräknar och sparar body_fat_pct - Fixar tomma numeriska fält (null istället för '') - Döljer 1RM för nybörjare
This commit is contained in:
+95
-6
@@ -69,9 +69,31 @@ app.post('/api/auth/login', async (req, res) => {
|
|||||||
|
|
||||||
app.get('/api/user/profile', authMiddleware, async (req, res) => {
|
app.get('/api/user/profile', authMiddleware, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const result = await pool.query('SELECT id, email, gender, age, weight, neck_cm, waist_cm, hip_cm, experience_level, bench_1rm, squat_1rm, deadlift_1rm, goal, workouts_per_week, onboarding_complete FROM users WHERE id = $1', [req.user.id]);
|
const userResult = await pool.query(
|
||||||
if (!result.rows.length) return res.status(404).json({ error: 'User not found' });
|
'SELECT id, email, gender, age, height_cm, experience_level, goal, workouts_per_week, onboarding_complete FROM users WHERE id = $1',
|
||||||
res.json(result.rows[0]);
|
[req.user.id]
|
||||||
|
);
|
||||||
|
if (!userResult.rows.length) return res.status(404).json({ error: 'User not found' });
|
||||||
|
|
||||||
|
const user = userResult.rows[0];
|
||||||
|
|
||||||
|
// Get latest measurements
|
||||||
|
const measResult = await pool.query(
|
||||||
|
'SELECT weight, neck_cm, waist_cm, hip_cm, body_fat_pct, measured_at FROM user_measurements WHERE user_id = $1 ORDER BY measured_at DESC LIMIT 1',
|
||||||
|
[req.user.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get latest strength
|
||||||
|
const strResult = await pool.query(
|
||||||
|
'SELECT bench_1rm, squat_1rm, deadlift_1rm, measured_at FROM user_strength WHERE user_id = $1 ORDER BY measured_at DESC LIMIT 1',
|
||||||
|
[req.user.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
...user,
|
||||||
|
measurements: measResult.rows[0] || null,
|
||||||
|
strength: strResult.rows[0] || null
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Profile error:', err);
|
console.error('Profile error:', err);
|
||||||
res.status(500).json({ error: 'Server error' });
|
res.status(500).json({ error: 'Server error' });
|
||||||
@@ -80,10 +102,13 @@ app.get('/api/user/profile', authMiddleware, async (req, res) => {
|
|||||||
|
|
||||||
app.put('/api/user/profile', authMiddleware, async (req, res) => {
|
app.put('/api/user/profile', authMiddleware, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { gender, age, weight, neck_cm, waist_cm, hip_cm, experience_level, bench_1rm, squat_1rm, deadlift_1rm, goal, workouts_per_week, onboarding_complete } = req.body;
|
const { gender, age, height_cm, experience_level, goal, workouts_per_week, onboarding_complete } = req.body;
|
||||||
|
const num = v => (v === '' || v === undefined) ? null : v;
|
||||||
|
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
`UPDATE users SET gender=$1, age=$2, weight=$3, neck_cm=$4, waist_cm=$5, hip_cm=$6, experience_level=$7, bench_1rm=$8, squat_1rm=$9, deadlift_1rm=$10, goal=$11, workouts_per_week=$12, onboarding_complete=$13 WHERE id=$14 RETURNING id, email, gender, age, weight, neck_cm, waist_cm, hip_cm, experience_level, bench_1rm, squat_1rm, deadlift_1rm, goal, workouts_per_week, onboarding_complete`,
|
`UPDATE users SET gender=$1, age=$2, height_cm=$3, experience_level=$4, goal=$5, workouts_per_week=$6, onboarding_complete=$7
|
||||||
[gender, age, weight, neck_cm, waist_cm, hip_cm, experience_level, bench_1rm, squat_1rm, deadlift_1rm, goal, workouts_per_week, onboarding_complete, req.user.id]
|
WHERE id=$8 RETURNING id, email, gender, age, height_cm, experience_level, goal, workouts_per_week, onboarding_complete`,
|
||||||
|
[gender, num(age), num(height_cm), experience_level, goal, num(workouts_per_week), onboarding_complete, req.user.id]
|
||||||
);
|
);
|
||||||
res.json(result.rows[0]);
|
res.json(result.rows[0]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -92,6 +117,70 @@ app.put('/api/user/profile', authMiddleware, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add measurements
|
||||||
|
app.post('/api/user/measurements', authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { weight, neck_cm, waist_cm, hip_cm, body_fat_pct } = req.body;
|
||||||
|
const num = v => (v === '' || v === undefined) ? null : v;
|
||||||
|
|
||||||
|
const result = await pool.query(
|
||||||
|
`INSERT INTO user_measurements (user_id, weight, neck_cm, waist_cm, hip_cm, body_fat_pct)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6) RETURNING *`,
|
||||||
|
[req.user.id, num(weight), num(neck_cm), num(waist_cm), num(hip_cm), num(body_fat_pct)]
|
||||||
|
);
|
||||||
|
res.json(result.rows[0]);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Add measurements error:', err);
|
||||||
|
res.status(500).json({ error: 'Server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get measurements history
|
||||||
|
app.get('/api/user/measurements', authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await pool.query(
|
||||||
|
'SELECT * FROM user_measurements WHERE user_id = $1 ORDER BY measured_at DESC LIMIT 100',
|
||||||
|
[req.user.id]
|
||||||
|
);
|
||||||
|
res.json(result.rows);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Get measurements error:', err);
|
||||||
|
res.status(500).json({ error: 'Server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add strength record
|
||||||
|
app.post('/api/user/strength', authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { bench_1rm, squat_1rm, deadlift_1rm } = req.body;
|
||||||
|
const num = v => (v === '' || v === undefined) ? null : v;
|
||||||
|
|
||||||
|
const result = await pool.query(
|
||||||
|
`INSERT INTO user_strength (user_id, bench_1rm, squat_1rm, deadlift_1rm)
|
||||||
|
VALUES ($1, $2, $3, $4) RETURNING *`,
|
||||||
|
[req.user.id, num(bench_1rm), num(squat_1rm), num(deadlift_1rm)]
|
||||||
|
);
|
||||||
|
res.json(result.rows[0]);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Add strength error:', err);
|
||||||
|
res.status(500).json({ error: 'Server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get strength history
|
||||||
|
app.get('/api/user/strength', authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await pool.query(
|
||||||
|
'SELECT * FROM user_strength WHERE user_id = $1 ORDER BY measured_at DESC LIMIT 100',
|
||||||
|
[req.user.id]
|
||||||
|
);
|
||||||
|
res.json(result.rows);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Get strength error:', err);
|
||||||
|
res.status(500).json({ error: 'Server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Get all programs
|
// Get all programs
|
||||||
app.get('/api/programs', async (req, res) => {
|
app.get('/api/programs', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -64,8 +64,10 @@ export function AuthProvider({ children }) {
|
|||||||
setUser(null);
|
setUser(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const refreshProfile = () => fetchProfile();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider value={{ user, token, loading, register, login, logout, updateProfile }}>
|
<AuthContext.Provider value={{ user, token, loading, register, login, logout, updateProfile, refreshProfile }}>
|
||||||
{children}
|
{children}
|
||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { useState } from 'react';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
|
|
||||||
|
const API = '/api';
|
||||||
|
|
||||||
const calcBodyFat = (gender, waist, neck, hip, height) => {
|
const calcBodyFat = (gender, waist, neck, hip, height) => {
|
||||||
if (!waist || !neck || !height) return null;
|
if (!waist || !neck || !height) return null;
|
||||||
if (gender === 'female' && !hip) return null;
|
if (gender === 'female' && !hip) return null;
|
||||||
@@ -13,7 +15,7 @@ export default function OnboardingWizard() {
|
|||||||
const [step, setStep] = useState(1);
|
const [step, setStep] = useState(1);
|
||||||
const [data, setData] = useState({ gender: '', age: '', height_cm: '', weight: '', neck_cm: '', waist_cm: '', hip_cm: '', experience_level: '', bench_1rm: '', squat_1rm: '', deadlift_1rm: '', goal: '', workouts_per_week: '' });
|
const [data, setData] = useState({ gender: '', age: '', height_cm: '', weight: '', neck_cm: '', waist_cm: '', hip_cm: '', experience_level: '', bench_1rm: '', squat_1rm: '', deadlift_1rm: '', goal: '', workouts_per_week: '' });
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const { updateProfile } = useAuth();
|
const { token, updateProfile, refreshProfile } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const update = (field, value) => setData(d => ({ ...d, [field]: value }));
|
const update = (field, value) => setData(d => ({ ...d, [field]: value }));
|
||||||
@@ -21,8 +23,52 @@ export default function OnboardingWizard() {
|
|||||||
|
|
||||||
const finish = async () => {
|
const finish = async () => {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
await updateProfile({ ...data, onboarding_complete: true });
|
try {
|
||||||
navigate('/');
|
// 1. Save profile
|
||||||
|
await updateProfile({
|
||||||
|
gender: data.gender,
|
||||||
|
age: data.age,
|
||||||
|
height_cm: data.height_cm,
|
||||||
|
experience_level: data.experience_level,
|
||||||
|
goal: data.goal,
|
||||||
|
workouts_per_week: data.workouts_per_week,
|
||||||
|
onboarding_complete: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Save measurements
|
||||||
|
if (data.weight || data.neck_cm || data.waist_cm) {
|
||||||
|
await fetch(`${API}/user/measurements`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
||||||
|
body: JSON.stringify({
|
||||||
|
weight: data.weight,
|
||||||
|
neck_cm: data.neck_cm,
|
||||||
|
waist_cm: data.waist_cm,
|
||||||
|
hip_cm: data.hip_cm,
|
||||||
|
body_fat_pct: bodyFat
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Save strength if provided
|
||||||
|
if (data.bench_1rm || data.squat_1rm || data.deadlift_1rm) {
|
||||||
|
await fetch(`${API}/user/strength`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
||||||
|
body: JSON.stringify({
|
||||||
|
bench_1rm: data.bench_1rm,
|
||||||
|
squat_1rm: data.squat_1rm,
|
||||||
|
deadlift_1rm: data.deadlift_1rm
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (refreshProfile) await refreshProfile();
|
||||||
|
navigate('/');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Onboarding error:', err);
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -97,12 +143,16 @@ export default function OnboardingWizard() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="hint">1RM (valfritt)</p>
|
{(data.experience_level === 'intermediate' || data.experience_level === 'advanced') && (
|
||||||
<div className="rm-fields">
|
<>
|
||||||
<div className="field"><label>Bänk</label><input type="number" value={data.bench_1rm} onChange={e => update('bench_1rm', e.target.value)} placeholder="kg" /></div>
|
<p className="hint">1RM (valfritt)</p>
|
||||||
<div className="field"><label>Knäböj</label><input type="number" value={data.squat_1rm} onChange={e => update('squat_1rm', e.target.value)} placeholder="kg" /></div>
|
<div className="rm-fields">
|
||||||
<div className="field"><label>Marklyft</label><input type="number" value={data.deadlift_1rm} onChange={e => update('deadlift_1rm', e.target.value)} placeholder="kg" /></div>
|
<div className="field"><label>Bänk</label><input type="number" value={data.bench_1rm} onChange={e => update('bench_1rm', e.target.value)} placeholder="kg" /></div>
|
||||||
</div>
|
<div className="field"><label>Knäböj</label><input type="number" value={data.squat_1rm} onChange={e => update('squat_1rm', e.target.value)} placeholder="kg" /></div>
|
||||||
|
<div className="field"><label>Marklyft</label><input type="number" value={data.deadlift_1rm} onChange={e => update('deadlift_1rm', e.target.value)} placeholder="kg" /></div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<div className="nav-btns">
|
<div className="nav-btns">
|
||||||
<button onClick={() => setStep(2)}>← Tillbaka</button>
|
<button onClick={() => setStep(2)}>← Tillbaka</button>
|
||||||
<button className="next-btn" onClick={() => setStep(4)} disabled={!data.experience_level}>Nästa →</button>
|
<button className="next-btn" onClick={() => setStep(4)} disabled={!data.experience_level}>Nästa →</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user