feat: Add customizable completion modal for checklist
Co-authored-by: mathieumoran1 <[email protected]>
This commit is contained in:
co-authored by
mathieumoran1
parent
ef97ff0730
commit
a1c781e8a5
@@ -112,8 +112,11 @@
|
|||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body text-center">
|
<div class="modal-body text-center">
|
||||||
<video id="rewardVideo" width="75%" controls>
|
<div id="completion-modal-custom" class="text-start d-none"></div>
|
||||||
<source src="video/reward.mp4" type="video/mp4">
|
<div id="completion-modal-text" class="text-start mb-3" style="white-space: pre-wrap;"></div>
|
||||||
|
|
||||||
|
<video id="rewardVideo" width="75%" controls class="d-none">
|
||||||
|
<source id="rewardVideoSource" src="video/reward.mp4" type="video/mp4">
|
||||||
Votre navigateur ne supporte pas la vidéo.
|
Votre navigateur ne supporte pas la vidéo.
|
||||||
</video>
|
</video>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,14 @@ const Movie = require("../models/Movie");
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const ACTIVE_YEAR_KEY = "active_oscar_year";
|
const ACTIVE_YEAR_KEY = "active_oscar_year";
|
||||||
|
const COMPLETION_MODAL_KEY = "completion_modal_content";
|
||||||
|
|
||||||
|
const DEFAULT_COMPLETION_MODAL = Object.freeze({
|
||||||
|
title: "Félicitations! very nice 🎉🎉🎉",
|
||||||
|
bodyText: "",
|
||||||
|
videoSrc: "video/reward.mp4",
|
||||||
|
bodyHtml: "",
|
||||||
|
});
|
||||||
|
|
||||||
function parseOscarYear(raw) {
|
function parseOscarYear(raw) {
|
||||||
if (raw === undefined || raw === null || raw === "") return null;
|
if (raw === undefined || raw === null || raw === "") return null;
|
||||||
@@ -16,6 +24,34 @@ function parseOscarYear(raw) {
|
|||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sanitizeHtml(raw) {
|
||||||
|
// Basic, defensive sanitization (admin-controlled input, rendered for all users).
|
||||||
|
// We remove script tags and inline event handlers. This is not a complete HTML sanitizer.
|
||||||
|
const html = String(raw || "");
|
||||||
|
const withoutScripts = html.replace(/<script\b[\s\S]*?<\/script>/gi, "");
|
||||||
|
const withoutOnAttrs = withoutScripts
|
||||||
|
.replace(/\son\w+="[^"]*"/gi, "")
|
||||||
|
.replace(/\son\w+='[^']*'/gi, "")
|
||||||
|
.replace(/\son\w+=\S+/gi, "");
|
||||||
|
return withoutOnAttrs.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeCompletionModal(rawValue) {
|
||||||
|
const value = rawValue && typeof rawValue === "object" && !Array.isArray(rawValue) ? rawValue : {};
|
||||||
|
|
||||||
|
const title = typeof value.title === "string" ? value.title : DEFAULT_COMPLETION_MODAL.title;
|
||||||
|
const bodyText = typeof value.bodyText === "string" ? value.bodyText : DEFAULT_COMPLETION_MODAL.bodyText;
|
||||||
|
const videoSrc = typeof value.videoSrc === "string" ? value.videoSrc : DEFAULT_COMPLETION_MODAL.videoSrc;
|
||||||
|
const bodyHtml = typeof value.bodyHtml === "string" ? value.bodyHtml : DEFAULT_COMPLETION_MODAL.bodyHtml;
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: String(title || "").slice(0, 200).trim() || DEFAULT_COMPLETION_MODAL.title,
|
||||||
|
bodyText: String(bodyText || "").slice(0, 8000),
|
||||||
|
videoSrc: String(videoSrc || "").slice(0, 2048).trim(),
|
||||||
|
bodyHtml: sanitizeHtml(String(bodyHtml || "").slice(0, 20000)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function getOrInitActiveYear() {
|
async function getOrInitActiveYear() {
|
||||||
const existing = await Setting.findOne({ key: ACTIVE_YEAR_KEY });
|
const existing = await Setting.findOne({ key: ACTIVE_YEAR_KEY });
|
||||||
if (existing && typeof existing.value === "number") return existing.value;
|
if (existing && typeof existing.value === "number") return existing.value;
|
||||||
@@ -78,5 +114,44 @@ router.put("/year", async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Public: get the "100% completion" modal content for checklist.
|
||||||
|
router.get("/completion-modal", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const existing = await Setting.findOne({ key: COMPLETION_MODAL_KEY }).select("value");
|
||||||
|
const normalized = normalizeCompletionModal(existing?.value);
|
||||||
|
return res.status(200).json(normalized);
|
||||||
|
} catch (err) {
|
||||||
|
// If DB is down, still return a sane default so the client can render.
|
||||||
|
return res.status(200).json(DEFAULT_COMPLETION_MODAL);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Admin-only: set the "100% completion" modal content for checklist.
|
||||||
|
router.put("/completion-modal", async (req, res) => {
|
||||||
|
const token = req.headers.authorization?.split(" ")[1];
|
||||||
|
if (!token) return res.status(401).json({ message: "Authentication token is required" });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||||
|
if (!decoded.admin) {
|
||||||
|
return res.status(403).json({ message: "You do not have admin privileges" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = normalizeCompletionModal(req.body);
|
||||||
|
const updated = await Setting.findOneAndUpdate(
|
||||||
|
{ key: COMPLETION_MODAL_KEY },
|
||||||
|
{ $set: { value: normalized } },
|
||||||
|
{ upsert: true, new: true }
|
||||||
|
).select("value");
|
||||||
|
|
||||||
|
return res.status(200).json(normalizeCompletionModal(updated?.value));
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof jwt.JsonWebTokenError) {
|
||||||
|
return res.status(401).json({ message: "Invalid or expired token" });
|
||||||
|
}
|
||||||
|
return res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user