diff --git a/backend/src/controllers/courseController.js b/backend/src/controllers/courseController.js index 974ce88..e61f5c1 100644 --- a/backend/src/controllers/courseController.js +++ b/backend/src/controllers/courseController.js @@ -23,6 +23,7 @@ function courseRow(row) { title: row.title, description: row.description || '', coverEmoji: row.cover_emoji, + coverImage: row.cover_image || null, orderIndex: row.order_index, isPublished: row.is_published === 1, createdBy: row.created_by, @@ -403,14 +404,15 @@ function duplicate(req, res) { function update(req, res) { const row = db.prepare('SELECT * FROM courses WHERE id = ?').get(req.params.id); if (!row) return res.status(404).json({ error: 'Course not found' }); - const { title, description, coverEmoji, orderIndex, isPublished, subjectSlug } = req.body; + const { title, description, coverEmoji, coverImage, orderIndex, isPublished, subjectSlug } = req.body; db.prepare(` - UPDATE courses SET title=?,description=?,cover_emoji=?,order_index=?,is_published=?,subject_slug=? WHERE id=? + UPDATE courses SET title=?,description=?,cover_emoji=?,cover_image=?,order_index=?,is_published=?,subject_slug=? WHERE id=? `).run( title ?? row.title, description !== undefined ? description : row.description, coverEmoji ?? row.cover_emoji, + coverImage !== undefined ? (coverImage || null) : row.cover_image, orderIndex ?? row.order_index, isPublished !== undefined ? (isPublished ? 1 : 0) : row.is_published, subjectSlug ?? row.subject_slug, diff --git a/backend/src/controllers/petController.js b/backend/src/controllers/petController.js index fd57f03..59e477f 100644 --- a/backend/src/controllers/petController.js +++ b/backend/src/controllers/petController.js @@ -178,7 +178,7 @@ function getPet(req, res) { const user = db.prepare( `SELECT xp, level, streak_current, streak_best, streak_date, coins, pet_name, last_login, pet_color, pet_last_petted, pet_petting_streak, - pet_bg, pet_bg_owned, pet_last_fed, pet_equipped, pet_pattern + pet_bg, pet_bg_owned, pet_last_fed, pet_equipped, pet_pattern, pet_bg_custom FROM users WHERE id = ?` ).get(req.user.id); @@ -277,6 +277,7 @@ function getPet(req, res) { feedCooldown, petBg: user.pet_bg || 'default', petBgOwned: _parseOwned(user.pet_bg_owned), + petBgCustom: user.pet_bg_custom || null, }); } @@ -391,7 +392,10 @@ function buyBg(req, res) { /* ── PATCH /api/pet/bg ────────────────────────────────────────────────── */ function setBg(req, res) { const { id } = req.body; - if (id !== 'default') { + if (id === 'custom') { + const url = db.prepare('SELECT pet_bg_custom FROM users WHERE id=?').get(req.user.id)?.pet_bg_custom; + if (!url) return res.status(400).json({ error: 'no_custom_bg' }); + } else if (id !== 'default') { const owned = _parseOwned(db.prepare('SELECT pet_bg_owned FROM users WHERE id=?').get(req.user.id)?.pet_bg_owned); if (!owned.includes(id)) return res.status(403).json({ error: 'not owned' }); } @@ -399,6 +403,18 @@ function setBg(req, res) { res.json({ ok: true, bg: id }); } +/* ── POST /api/pet/bg/custom ────────────────────────────────────────────── + Сохранить сгенерированную ИИ картинку как кастомный фон и сделать активной. + URL принимается только из /uploads/generated/ (то, что отдаёт /api/imggen). */ +function setCustomBg(req, res) { + const url = String(req.body && req.body.url || ''); + if (!/^\/uploads\/generated\/[A-Za-z0-9._-]+\.(png|jpg|jpeg|webp)$/.test(url)) { + return res.status(400).json({ error: 'invalid_url' }); + } + db.prepare('UPDATE users SET pet_bg_custom=?, pet_bg=? WHERE id=?').run(url, 'custom', req.user.id); + res.json({ ok: true, bg: 'custom', url }); +} + /* ── POST /api/pet/star ───────────────────────────────────────────────── */ function starCatch(req, res) { const user = db.prepare('SELECT pet_last_star FROM users WHERE id=?').get(req.user.id); @@ -436,4 +452,4 @@ function feedPet(req, res) { res.json({ ok: true, xpAwarded: 15, xp: updated.xp, coins: updated.coins }); } -module.exports = { getPet, renamePet, petAction, updateColor, starCatch, getShop, buyBg, setBg, feedPet, equipAccessories, updatePattern }; +module.exports = { getPet, renamePet, petAction, updateColor, starCatch, getShop, buyBg, setBg, setCustomBg, feedPet, equipAccessories, updatePattern }; diff --git a/backend/src/db/migrations/068_pet_custom_bg.sql b/backend/src/db/migrations/068_pet_custom_bg.sql new file mode 100644 index 0000000..f461f04 --- /dev/null +++ b/backend/src/db/migrations/068_pet_custom_bg.sql @@ -0,0 +1,3 @@ +-- Кастомный фон питомца: URL сгенерированной ИИ картинки (/uploads/generated/...). +-- Когда pet_bg = 'custom', сцена использует это изображение. +ALTER TABLE users ADD COLUMN pet_bg_custom TEXT; diff --git a/backend/src/db/migrations/069_course_cover_image.sql b/backend/src/db/migrations/069_course_cover_image.sql new file mode 100644 index 0000000..6542229 --- /dev/null +++ b/backend/src/db/migrations/069_course_cover_image.sql @@ -0,0 +1,2 @@ +-- Обложка курса картинкой (URL). Если задана — показывается вместо эмодзи. +ALTER TABLE courses ADD COLUMN cover_image TEXT; diff --git a/backend/src/routes/pet.js b/backend/src/routes/pet.js index c6ece1d..b1914ce 100644 --- a/backend/src/routes/pet.js +++ b/backend/src/routes/pet.js @@ -12,6 +12,7 @@ router.post('/star', authMiddleware, c.starCatch); router.get('/shop', authMiddleware, c.getShop); router.post('/shop/buy', authMiddleware, c.buyBg); router.patch('/bg', authMiddleware, c.setBg); +router.post('/bg/custom', authMiddleware, c.setCustomBg); router.post('/feed', authMiddleware, c.feedPet); module.exports = router; diff --git a/frontend/classroom.html b/frontend/classroom.html index 6a49da7..e22edf3 100644 --- a/frontend/classroom.html +++ b/frontend/classroom.html @@ -2452,6 +2452,7 @@ +
@@ -3093,6 +3094,7 @@ + @@ -6557,45 +6559,64 @@ document.getElementById('wb-image-input')?.click(); } + // Поместить загруженный