Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-07-12 12:39:17 +08:00
parent 82ea9c77c4
commit ea3e82e54c
47 changed files with 1301 additions and 481 deletions
@@ -33,6 +33,10 @@ const Advertisement = sequelize.define("Advertisement", {
},
// ─── Content ──────────────────────────────────────────────────────────────
// "image" = image only, "content" = badge/headline/description/CTAs alongside
// the image — an explicit admin choice made in the creation wizard, decoupled
// from placement/format.
content_mode: { type: DataTypes.ENUM("image", "content"), allowNull: false, defaultValue: "image", label: "Content Mode", order: 3.5 },
badge_label: { type: DataTypes.STRING(100), label: "Badge Label", order: 4 },
headline: { type: DataTypes.STRING(255), label: "Headline", order: 5 },
description: { type: DataTypes.TEXT, label: "Description", order: 6 },
@@ -45,6 +49,15 @@ const Advertisement = sequelize.define("Advertisement", {
// [{ label, link }, ...] — 0-2 entries depending on type
ctas: { type: DataTypes.JSONB, allowNull: false, defaultValue: [], label: "CTAs", order: 7 },
// Where the ad as a whole links to when clicked with no CTA of its own
// (banners/full-image ads have no CTA row) — takes priority over landing_page.
redirect_link: { type: DataTypes.STRING(512), allowNull: true, label: "Redirect Link", order: 7.5 },
// Internally-authored landing page { title, description, body, links: [{label, link}] }
// used as the click-through destination when redirect_link is empty — see
// GET /api/client/advertisements/uuid/:uuid and the /ads/:uuid client route.
landing_page: { type: DataTypes.JSONB, allowNull: true, label: "Landing Page", order: 0, hidden: true },
// ─── Scheduling ───────────────────────────────────────────────────────────
start_date: { type: DataTypes.DATE, label: "Start Date", order: 8 },
end_date: { type: DataTypes.DATE, label: "End Date", order: 9 },
@@ -12,19 +12,10 @@
// in controllers/admin/advertisements.controller.js) — type is never accepted
// from the client once a placement is set.
// TODO(ads-1): Re-categorize placements — Hero -> Dashboard, Banner -> Tier Plans.
// Remove the "popup" and "sidebar" formats entirely (dashboard.popup,
// course_details.sidebar). Keep in sync with the frontend mirror at
// new_starr_app/src/data/placement.data.js. This also feeds the Step 1
// "Placement" picker in the Add Advertisement wizard (TODO(ads-6)), which
// should only offer Dashboard / Tier Plans / Course Details.
const PLACEMENTS = [
{ key: "dashboard.hero", format: "hero", page: "dashboard", pageLabel: "Dashboard", slotLabel: "Hero (top of page)" },
{ key: "dashboard.popup", format: "popup", page: "dashboard", pageLabel: "Dashboard", slotLabel: "Popup (on load)" },
{ key: "course_list.banner", format: "banner", page: "course_list", pageLabel: "Courses", slotLabel: "Banner (above course grid)" },
{ key: "tier_plans.banner", format: "banner", page: "tier_plans", pageLabel: "Tier Plans", slotLabel: "Banner (above plan cards)" },
{ key: "course_details.banner", format: "banner", page: "course_details", pageLabel: "Course Details", slotLabel: "Banner (below hero)" },
{ key: "course_details.sidebar", format: "sidebar", page: "course_details", pageLabel: "Course Details", slotLabel: "Sidebar (beside course content)" },
{ key: "plans.banner", format: "banner", page: "plans", pageLabel: "Plans", slotLabel: "Banner (above plan cards)" },
];
const PLACEMENT_MAP = Object.fromEntries(PLACEMENTS.map((p) => [p.key, p]));