Spaces:
Sleeping
Sleeping
Site Maintainer commited on
Commit ·
8776c0f
1
Parent(s): 8e21ae0
Ordering support: position field, append new items, admin position endpoints, sort by position+createdAt
Browse files- models/SourceText.js +2 -0
- routes/auth.js +58 -2
models/SourceText.js
CHANGED
|
@@ -32,6 +32,8 @@ const sourceTextSchema = new mongoose.Schema({
|
|
| 32 |
imageSize: { type: Number, default: 200 },
|
| 33 |
// Add 'portrait-split' for week 4–6 advanced layout (non-breaking, additive)
|
| 34 |
imageAlignment: { type: String, enum: ['left', 'center', 'right', 'portrait-split'], default: 'center' },
|
|
|
|
|
|
|
| 35 |
culturalElements: [culturalElementSchema],
|
| 36 |
difficulty: {
|
| 37 |
type: String,
|
|
|
|
| 32 |
imageSize: { type: Number, default: 200 },
|
| 33 |
// Add 'portrait-split' for week 4–6 advanced layout (non-breaking, additive)
|
| 34 |
imageAlignment: { type: String, enum: ['left', 'center', 'right', 'portrait-split'], default: 'center' },
|
| 35 |
+
// Optional display ordering within a week/category (ascending). If absent, fallback to createdAt.
|
| 36 |
+
position: { type: Number },
|
| 37 |
culturalElements: [culturalElementSchema],
|
| 38 |
difficulty: {
|
| 39 |
type: String,
|
routes/auth.js
CHANGED
|
@@ -160,7 +160,7 @@ router.delete('/admin/practice-examples/:id', authenticateToken, async (req, res
|
|
| 160 |
router.get('/admin/tutorial-tasks', authenticateToken, async (req, res) => {
|
| 161 |
try {
|
| 162 |
const SourceText = require('../models/SourceText');
|
| 163 |
-
const tutorialTasks = await SourceText.find({ category: 'tutorial' }).sort({ weekNumber: 1, createdAt:
|
| 164 |
res.json({ success: true, tutorialTasks });
|
| 165 |
} catch (error) {
|
| 166 |
console.error('Get tutorial tasks error:', error);
|
|
@@ -180,6 +180,19 @@ router.post('/admin/tutorial-tasks', authenticateToken, requireAdmin, async (req
|
|
| 180 |
if (!content || content.trim() === '') return res.status(400).json({ error: 'Content is required' });
|
| 181 |
}
|
| 182 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
const newTask = new SourceText({
|
| 184 |
content: content || (imageUrl ? 'Image-based task' : ''),
|
| 185 |
weekNumber: parseInt(weekNumber),
|
|
@@ -189,6 +202,7 @@ router.post('/admin/tutorial-tasks', authenticateToken, requireAdmin, async (req
|
|
| 189 |
sourceType: 'tutorial',
|
| 190 |
imageUrl,
|
| 191 |
imageAlt,
|
|
|
|
| 192 |
...(imageUrl && (!content || content.trim() === '') && { imageSize: imageSize || 200 }),
|
| 193 |
...(imageUrl && (!content || content.trim() === '') && { imageAlignment: imageAlignment || 'center' }),
|
| 194 |
translationBrief
|
|
@@ -202,6 +216,20 @@ router.post('/admin/tutorial-tasks', authenticateToken, requireAdmin, async (req
|
|
| 202 |
}
|
| 203 |
});
|
| 204 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
router.put('/admin/tutorial-tasks/:id', authenticateToken, requireAdmin, async (req, res) => {
|
| 206 |
try {
|
| 207 |
const SourceText = require('../models/SourceText');
|
|
@@ -236,7 +264,7 @@ router.delete('/admin/tutorial-tasks/:id', authenticateToken, requireAdmin, asyn
|
|
| 236 |
router.get('/admin/weekly-practice', authenticateToken, async (req, res) => {
|
| 237 |
try {
|
| 238 |
const SourceText = require('../models/SourceText');
|
| 239 |
-
const weeklyPractice = await SourceText.find({ category: 'weekly-practice' }).sort({ weekNumber: 1, createdAt:
|
| 240 |
res.json({ success: true, weeklyPractice });
|
| 241 |
} catch (error) {
|
| 242 |
console.error('Get weekly practice error:', error);
|
|
@@ -256,6 +284,19 @@ router.post('/admin/weekly-practice', authenticateToken, requireAdmin, async (re
|
|
| 256 |
if (!content || content.trim() === '') return res.status(400).json({ error: 'Content is required' });
|
| 257 |
}
|
| 258 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
const newPractice = new SourceText({
|
| 260 |
content: content || (imageUrl ? 'Image-based practice' : ''),
|
| 261 |
weekNumber: parseInt(weekNumber),
|
|
@@ -265,6 +306,7 @@ router.post('/admin/weekly-practice', authenticateToken, requireAdmin, async (re
|
|
| 265 |
sourceType: 'weekly-practice',
|
| 266 |
imageUrl,
|
| 267 |
imageAlt,
|
|
|
|
| 268 |
...(imageUrl && (!content || content.trim() === '') && { imageSize: imageSize || 200 }),
|
| 269 |
...(imageUrl && (!content || content.trim() === '') && { imageAlignment: imageAlignment || 'center' }),
|
| 270 |
translationBrief
|
|
@@ -278,6 +320,20 @@ router.post('/admin/weekly-practice', authenticateToken, requireAdmin, async (re
|
|
| 278 |
}
|
| 279 |
});
|
| 280 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
router.put('/admin/weekly-practice/:id', authenticateToken, requireAdmin, async (req, res) => {
|
| 282 |
try {
|
| 283 |
const SourceText = require('../models/SourceText');
|
|
|
|
| 160 |
router.get('/admin/tutorial-tasks', authenticateToken, async (req, res) => {
|
| 161 |
try {
|
| 162 |
const SourceText = require('../models/SourceText');
|
| 163 |
+
const tutorialTasks = await SourceText.find({ category: 'tutorial' }).sort({ weekNumber: 1, position: 1, createdAt: 1 });
|
| 164 |
res.json({ success: true, tutorialTasks });
|
| 165 |
} catch (error) {
|
| 166 |
console.error('Get tutorial tasks error:', error);
|
|
|
|
| 180 |
if (!content || content.trim() === '') return res.status(400).json({ error: 'Content is required' });
|
| 181 |
}
|
| 182 |
|
| 183 |
+
// Determine optional position for weeks 4-6 so newly added items append to the end
|
| 184 |
+
let position;
|
| 185 |
+
if (parseInt(weekNumber) >= 4) {
|
| 186 |
+
try {
|
| 187 |
+
const last = await SourceText.find({ category: 'tutorial', weekNumber: parseInt(weekNumber) })
|
| 188 |
+
.sort({ position: -1 })
|
| 189 |
+
.limit(1);
|
| 190 |
+
position = ((last[0] && typeof last[0].position === 'number') ? last[0].position : 0) + 1;
|
| 191 |
+
} catch (e) {
|
| 192 |
+
position = undefined;
|
| 193 |
+
}
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
const newTask = new SourceText({
|
| 197 |
content: content || (imageUrl ? 'Image-based task' : ''),
|
| 198 |
weekNumber: parseInt(weekNumber),
|
|
|
|
| 202 |
sourceType: 'tutorial',
|
| 203 |
imageUrl,
|
| 204 |
imageAlt,
|
| 205 |
+
...(position !== undefined ? { position } : {}),
|
| 206 |
...(imageUrl && (!content || content.trim() === '') && { imageSize: imageSize || 200 }),
|
| 207 |
...(imageUrl && (!content || content.trim() === '') && { imageAlignment: imageAlignment || 'center' }),
|
| 208 |
translationBrief
|
|
|
|
| 216 |
}
|
| 217 |
});
|
| 218 |
|
| 219 |
+
// Update position for a tutorial task (admin)
|
| 220 |
+
router.put('/admin/tutorial-tasks/:id/position', authenticateToken, requireAdmin, async (req, res) => {
|
| 221 |
+
try {
|
| 222 |
+
const SourceText = require('../models/SourceText');
|
| 223 |
+
const { position } = req.body;
|
| 224 |
+
const updated = await SourceText.findByIdAndUpdate(req.params.id, { position }, { new: true, runValidators: true });
|
| 225 |
+
if (!updated) return res.status(404).json({ error: 'Tutorial task not found' });
|
| 226 |
+
res.json({ success: true, message: 'Position updated', task: updated });
|
| 227 |
+
} catch (error) {
|
| 228 |
+
console.error('Update tutorial task position error:', error);
|
| 229 |
+
res.status(500).json({ error: 'Failed to update tutorial task position' });
|
| 230 |
+
}
|
| 231 |
+
});
|
| 232 |
+
|
| 233 |
router.put('/admin/tutorial-tasks/:id', authenticateToken, requireAdmin, async (req, res) => {
|
| 234 |
try {
|
| 235 |
const SourceText = require('../models/SourceText');
|
|
|
|
| 264 |
router.get('/admin/weekly-practice', authenticateToken, async (req, res) => {
|
| 265 |
try {
|
| 266 |
const SourceText = require('../models/SourceText');
|
| 267 |
+
const weeklyPractice = await SourceText.find({ category: 'weekly-practice' }).sort({ weekNumber: 1, position: 1, createdAt: 1 });
|
| 268 |
res.json({ success: true, weeklyPractice });
|
| 269 |
} catch (error) {
|
| 270 |
console.error('Get weekly practice error:', error);
|
|
|
|
| 284 |
if (!content || content.trim() === '') return res.status(400).json({ error: 'Content is required' });
|
| 285 |
}
|
| 286 |
|
| 287 |
+
// Determine optional position for weeks 4-6 so newly added items append to the end
|
| 288 |
+
let position;
|
| 289 |
+
if (parseInt(weekNumber) >= 4) {
|
| 290 |
+
try {
|
| 291 |
+
const last = await SourceText.find({ category: 'weekly-practice', weekNumber: parseInt(weekNumber) })
|
| 292 |
+
.sort({ position: -1 })
|
| 293 |
+
.limit(1);
|
| 294 |
+
position = ((last[0] && typeof last[0].position === 'number') ? last[0].position : 0) + 1;
|
| 295 |
+
} catch (e) {
|
| 296 |
+
position = undefined;
|
| 297 |
+
}
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
const newPractice = new SourceText({
|
| 301 |
content: content || (imageUrl ? 'Image-based practice' : ''),
|
| 302 |
weekNumber: parseInt(weekNumber),
|
|
|
|
| 306 |
sourceType: 'weekly-practice',
|
| 307 |
imageUrl,
|
| 308 |
imageAlt,
|
| 309 |
+
...(position !== undefined ? { position } : {}),
|
| 310 |
...(imageUrl && (!content || content.trim() === '') && { imageSize: imageSize || 200 }),
|
| 311 |
...(imageUrl && (!content || content.trim() === '') && { imageAlignment: imageAlignment || 'center' }),
|
| 312 |
translationBrief
|
|
|
|
| 320 |
}
|
| 321 |
});
|
| 322 |
|
| 323 |
+
// Update position for a weekly practice task (admin)
|
| 324 |
+
router.put('/admin/weekly-practice/:id/position', authenticateToken, requireAdmin, async (req, res) => {
|
| 325 |
+
try {
|
| 326 |
+
const SourceText = require('../models/SourceText');
|
| 327 |
+
const { position } = req.body;
|
| 328 |
+
const updated = await SourceText.findByIdAndUpdate(req.params.id, { position }, { new: true, runValidators: true });
|
| 329 |
+
if (!updated) return res.status(404).json({ error: 'Weekly practice not found' });
|
| 330 |
+
res.json({ success: true, message: 'Position updated', practice: updated });
|
| 331 |
+
} catch (error) {
|
| 332 |
+
console.error('Update weekly practice position error:', error);
|
| 333 |
+
res.status(500).json({ error: 'Failed to update weekly practice position' });
|
| 334 |
+
}
|
| 335 |
+
});
|
| 336 |
+
|
| 337 |
router.put('/admin/weekly-practice/:id', authenticateToken, requireAdmin, async (req, res) => {
|
| 338 |
try {
|
| 339 |
const SourceText = require('../models/SourceText');
|