bolt / src /routes /programs.js
Abhinay
Deploy Bolt Run backend (Express + Firestore) as Docker Space
d3477e4
Raw
History Blame Contribute Delete
4.66 kB
const express = require('express');
const { collections, FieldValue, docToObject } = require('../db/firestore');
const authMiddleware = require('../middleware/auth');
const router = express.Router();
router.use(authMiddleware);
function enrollmentDocId(userId, programId) {
return `${userId}_${programId}`;
}
async function annotateEnrollment(programDocs, userId) {
if (programDocs.length === 0) return [];
const enrollSnaps = await Promise.all(
programDocs.map((d) =>
collections.userPrograms.doc(enrollmentDocId(userId, d.id)).get()
)
);
return programDocs.map((doc, i) => {
const program = docToObject(doc);
const e = enrollSnaps[i];
program.is_enrolled = e.exists;
if (e.exists) {
const ed = e.data();
program.current_week = ed.current_week ?? 1;
program.is_active = ed.is_active ?? true;
program.enrolled_at = ed.enrolled_at?.toDate?.().toISOString() ?? null;
} else {
program.current_week = null;
program.is_active = null;
}
return program;
});
}
// GET /api/programs/me/active — must come before /:id
router.get('/me/active', async (req, res) => {
try {
const enrollSnap = await collections.userPrograms
.where('userId', '==', req.userId)
.where('is_active', '==', true)
.get();
const programs = await Promise.all(
enrollSnap.docs.map(async (e) => {
const ed = e.data();
const p = await collections.programs.doc(ed.programId).get();
if (!p.exists) return null;
const program = docToObject(p);
program.current_week = ed.current_week ?? 1;
program.enrolled_at = ed.enrolled_at?.toDate?.().toISOString() ?? null;
return program;
})
);
res.json({ programs: programs.filter(Boolean) });
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
});
// GET /api/programs
router.get('/', async (req, res) => {
const { difficulty, category } = req.query;
try {
let query = collections.programs;
if (difficulty) query = query.where('difficulty', '==', difficulty);
// Firestore can't do case-insensitive substring; for category we filter exact match.
if (category) query = query.where('category', '==', category);
const snap = await query.get();
const sorted = snap.docs.slice().sort((a, b) => {
const ad = a.data(), bd = b.data();
if ((bd.is_featured ? 1 : 0) !== (ad.is_featured ? 1 : 0))
return (bd.is_featured ? 1 : 0) - (ad.is_featured ? 1 : 0);
const at = ad.created_at?.toMillis?.() ?? 0;
const bt = bd.created_at?.toMillis?.() ?? 0;
return bt - at;
});
const programs = await annotateEnrollment(sorted, req.userId);
res.json({ programs });
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
});
// GET /api/programs/:id
router.get('/:id', async (req, res) => {
try {
const snap = await collections.programs.doc(req.params.id).get();
if (!snap.exists) return res.status(404).json({ message: 'Program not found' });
const [program] = await annotateEnrollment([snap], req.userId);
res.json({ program });
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
});
// POST /api/programs/:id/enroll
router.post('/:id/enroll', async (req, res) => {
try {
const enrollRef = collections.userPrograms.doc(enrollmentDocId(req.userId, req.params.id));
const programRef = collections.programs.doc(req.params.id);
const enrollSnap = await enrollRef.get();
const wasEnrolled = enrollSnap.exists;
await enrollRef.set(
{
userId: req.userId,
programId: req.params.id,
enrolled_at: enrollSnap.exists ? enrollSnap.data().enrolled_at : FieldValue.serverTimestamp(),
current_week: 1,
is_active: true,
completed_at: null,
},
{ merge: true }
);
if (!wasEnrolled) {
await programRef.update({ total_enrolled: FieldValue.increment(1) });
}
res.json({ message: 'Enrolled successfully' });
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
});
// DELETE /api/programs/:id/enroll
router.delete('/:id/enroll', async (req, res) => {
try {
const enrollRef = collections.userPrograms.doc(enrollmentDocId(req.userId, req.params.id));
await enrollRef.set({ is_active: false }, { merge: true });
res.json({ message: 'Unenrolled' });
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Server error' });
}
});
module.exports = router;