mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
add: more commits
Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Users, GitFork, FolderOpen, BookText, ListCheck, ShieldCheck, Megaphone } from "lucide-react";
|
||||
import { Users, GitFork, FolderOpen, BookText, ListCheck, ShieldCheck, Megaphone, Bell, Trophy, Mail } from "lucide-react";
|
||||
|
||||
export const ADMIN_SECTIONS = [
|
||||
{
|
||||
@@ -39,6 +39,9 @@ export const ADMIN_SECTIONS = [
|
||||
description: "Manage public-facing content",
|
||||
tiles: [
|
||||
{ key: "advertisements", label: "Advertisements", icon: Megaphone, link: "/admin/advertisements" },
|
||||
{ key: "notifications", label: "Notifications", icon: Bell, link: "/admin/notifications" },
|
||||
{ key: "achievements", label: "Achievements", icon: Trophy, link: "/admin/achievements" },
|
||||
{ key: "email-templates", label: "Email Templates", icon: Mail, link: "/admin/email-templates" },
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,24 @@
|
||||
// data/cronPresets.data.js
|
||||
// Mirrors backend data/cronPresets.data.js — the only schedule options the UI offers.
|
||||
|
||||
export const CRON_PRESET_OPTIONS = [
|
||||
{ value: "every_minute", label: "Every minute" },
|
||||
{ value: "every_5_min", label: "Every 5 minutes" },
|
||||
{ value: "every_15_min", label: "Every 15 minutes" },
|
||||
{ value: "hourly", label: "Hourly" },
|
||||
{ value: "every_6_hours", label: "Every 6 hours" },
|
||||
{ value: "daily", label: "Daily at midnight" },
|
||||
];
|
||||
|
||||
export const CRON_PRESET_MAP = Object.fromEntries(
|
||||
CRON_PRESET_OPTIONS.map((p) => [p.value, p])
|
||||
);
|
||||
|
||||
// job_name -> friendly copy (label/description come from the API too, but this
|
||||
// is used as a fallback and for the settings icon).
|
||||
export const JOB_LABELS = {
|
||||
taskOverdue: { label: "Task Overdue Alerts (Admin)", description: "Notifies admins when tasks flip to overdue." },
|
||||
userNotifications: { label: "Task Overdue Alerts (Users)", description: "Notifies affected users when their tasks are marked overdue." },
|
||||
issueCertificates: { label: "Certificate Issued", description: "Notifies users when a course certificate is ready." },
|
||||
expireUserTiers: { label: "Tier Expired", description: "Notifies users when their subscription tier expires." },
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
export const EMAIL_BROADCAST_STATUSES = [
|
||||
{ value: "queued", label: "Queued", badgeClass: "bg-muted text-muted-foreground border-border" },
|
||||
{ value: "sending", label: "Sending", badgeClass: "bg-blue-100 text-blue-700 border-blue-400 dark:bg-blue-900/40 dark:text-blue-400 dark:border-blue-700" },
|
||||
{ value: "completed", label: "Completed", badgeClass: "bg-emerald-100 text-emerald-700 border-emerald-400 dark:bg-emerald-900/40 dark:text-emerald-400 dark:border-emerald-700" },
|
||||
{ value: "canceled", label: "Canceled", badgeClass: "bg-muted text-muted-foreground border-border" },
|
||||
];
|
||||
|
||||
export const EMAIL_BROADCAST_STATUS_MAP = Object.fromEntries(
|
||||
EMAIL_BROADCAST_STATUSES.map((s) => [s.value, s])
|
||||
);
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Megaphone, BadgePercent, Shield, Tag } from "lucide-react";
|
||||
|
||||
// Purely organizational — lets admins tell at a glance what an email template
|
||||
// is for. Independent of `is_system` (which is about whether the type/row can
|
||||
// be renamed or deleted, not what it's used for).
|
||||
export const EMAIL_TEMPLATE_CATEGORIES = [
|
||||
{
|
||||
value: "announcement",
|
||||
label: "Announcement",
|
||||
description: "Platform news, updates, or broadcast-style messages to users.",
|
||||
icon: Megaphone,
|
||||
badgeClass: "bg-blue-100 text-blue-700 border-blue-400 dark:bg-blue-900/40 dark:text-blue-400 dark:border-blue-700",
|
||||
},
|
||||
{
|
||||
value: "advertisement",
|
||||
label: "Advertisement",
|
||||
description: "Promotional or marketing content (offers, plans, campaigns).",
|
||||
icon: BadgePercent,
|
||||
badgeClass: "bg-amber-100 text-amber-700 border-amber-400 dark:bg-amber-900/40 dark:text-amber-400 dark:border-amber-700",
|
||||
},
|
||||
{
|
||||
value: "system",
|
||||
label: "System",
|
||||
description: "Account and transactional emails triggered by platform events.",
|
||||
icon: Shield,
|
||||
badgeClass: "bg-slate-100 text-slate-700 border-slate-400 dark:bg-slate-800/60 dark:text-slate-300 dark:border-slate-600",
|
||||
},
|
||||
{
|
||||
value: "other",
|
||||
label: "Other",
|
||||
description: "Anything that doesn't fit the categories above.",
|
||||
icon: Tag,
|
||||
badgeClass: "bg-purple-100 text-purple-700 border-purple-400 dark:bg-purple-900/40 dark:text-purple-400 dark:border-purple-700",
|
||||
},
|
||||
];
|
||||
|
||||
export const getEmailTemplateCategory = (value) =>
|
||||
EMAIL_TEMPLATE_CATEGORIES.find((c) => c.value === value) ?? EMAIL_TEMPLATE_CATEGORIES[3];
|
||||
|
||||
// Mirrors BROADCASTABLE_CATEGORIES in controllers/admin/email_broadcasts.controller.js —
|
||||
// only these categories make sense to blast to real recipients. System/transactional
|
||||
// templates are triggered per-user by app events, never mass-sent.
|
||||
export const BROADCASTABLE_CATEGORIES = ["announcement", "advertisement"];
|
||||
|
||||
export const isBroadcastable = (template) =>
|
||||
BROADCASTABLE_CATEGORIES.includes(template?.category) && template?.status === "sent";
|
||||
@@ -0,0 +1,14 @@
|
||||
// Reference-only registry of {{placeholder}} tokens available per system email
|
||||
// type. Purely informational for the admin editor — the backend derives the
|
||||
// real substitution data from wherever sendEmail({ type, data }) is called in
|
||||
// code, this just tells the admin what's actually available to reference.
|
||||
export const EMAIL_TEMPLATE_PLACEHOLDERS = {
|
||||
OTP: ["otp", "expiryMinutes"],
|
||||
WELCOME: ["name"],
|
||||
PASSWORD_CHANGED: [],
|
||||
ADDED_TO_GROUP: ["groupName"],
|
||||
TASK_ASSIGNED: ["taskTitle", "dueDate"],
|
||||
BAN_LIFTED: ["name", "email", "date"],
|
||||
BANNED: ["name", "email", "date", "reason", "duration_word", "duration_label", "suspension_note"],
|
||||
ADD_STAFF: ["name", "email", "password", "expiryHours"],
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
// A template is "sent" once it has live subject/html_body — that's the only
|
||||
// content services/email.service.js's sendEmail() ever reads on the backend.
|
||||
// Editing a sent template writes to draft_subject/draft_html_body instead, so
|
||||
// "pending changes" means there's a draft sitting on top of the live version.
|
||||
export const hasPendingChanges = (t) =>
|
||||
t?.status === "sent" && (t?.draft_subject != null || t?.draft_html_body != null);
|
||||
|
||||
export const STATUS_META = {
|
||||
draft: { label: "Draft", badgeClass: "bg-muted text-muted-foreground border-border" },
|
||||
sent: { label: "Sent", badgeClass: "bg-emerald-100 text-emerald-700 border-emerald-400 dark:bg-emerald-900/40 dark:text-emerald-400 dark:border-emerald-700" },
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
// data/notificationBroadcast.data.js
|
||||
import { Shield, Users, Megaphone, ListCheck, BookText, ShieldCheck } from "lucide-react";
|
||||
|
||||
// ─── Target types ─────────────────────────────────────────────────────────────
|
||||
// Drives: target select in the Add form, target badge on each card.
|
||||
// needsTarget: true means the form must also show a picker for target_id.
|
||||
|
||||
export const TARGET_TYPE_OPTIONS = [
|
||||
{ value: "admin", label: "Admins", icon: Shield, description: "Visible only in the admin notification bell", needsTarget: false },
|
||||
{ value: "user", label: "Users", icon: Users, description: "Sent to every active user", needsTarget: false },
|
||||
{ value: "both", label: "Admins & Users", icon: Megaphone, description: "Sent to admins and every active user", needsTarget: false },
|
||||
{ value: "task_list", label: "Task List", icon: ListCheck, description: "Sent to everyone assigned to a specific task list", needsTarget: true },
|
||||
{ value: "course", label: "Course", icon: BookText, description: "Sent to everyone with access to a specific course", needsTarget: true },
|
||||
{ value: "tier_plan", label: "Tier Plan", icon: ShieldCheck, description: "Sent to everyone currently on a specific tier plan", needsTarget: true },
|
||||
];
|
||||
|
||||
export const TARGET_TYPE_MAP = Object.fromEntries(
|
||||
TARGET_TYPE_OPTIONS.map((a) => [a.value, a])
|
||||
);
|
||||
|
||||
// ─── Statuses ───────────────────────────────────────────────────────────────
|
||||
// Drives: filter dropdown options, status badge color/label on each card.
|
||||
|
||||
export const BROADCAST_STATUSES = [
|
||||
{ value: "draft", label: "Draft", badgeClass: "bg-muted text-muted-foreground" },
|
||||
{ value: "sent", label: "Sent", badgeClass: "bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-400" },
|
||||
{ value: "archived", label: "Archived", badgeClass: "bg-muted text-muted-foreground" },
|
||||
];
|
||||
|
||||
export const BROADCAST_STATUS_MAP = Object.fromEntries(
|
||||
BROADCAST_STATUSES.map((s) => [s.value, s])
|
||||
);
|
||||
@@ -0,0 +1,27 @@
|
||||
// data/placement.data.js
|
||||
//
|
||||
// Mirrors the backend placement registry (models/advertisements/advertisements.placements.js
|
||||
// in new_starr). Each entry is a page + position slot; the format (hero/banner/popup/sidebar)
|
||||
// is derived from the placement, never chosen independently. Keep this list in sync with the
|
||||
// backend registry when adding a new placement — same pattern as ADVERTISEMENT_TYPES already
|
||||
// mirroring the backend type ENUM.
|
||||
|
||||
export 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: "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)" },
|
||||
];
|
||||
|
||||
export const PLACEMENT_MAP = Object.fromEntries(PLACEMENTS.map((p) => [p.key, p]));
|
||||
|
||||
// Grouped by page — powers the cascading Page → Position picker in Add/EditAdvertisement.
|
||||
export const AD_PAGES = Object.values(
|
||||
PLACEMENTS.reduce((acc, p) => {
|
||||
if (!acc[p.page]) acc[p.page] = { page: p.page, pageLabel: p.pageLabel, placements: [] };
|
||||
acc[p.page].placements.push(p);
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
@@ -0,0 +1,75 @@
|
||||
// data/placementLayouts.data.js
|
||||
//
|
||||
// Declarative "wireframe recipe" per placement — powers <PlacementSkeleton />
|
||||
// in the advertisement creation wizard so admins can see roughly where their
|
||||
// ad will land before saving. Keys mirror PLACEMENTS in placement.data.js.
|
||||
// Adding a new placement: add one entry here, nothing else needs to change.
|
||||
//
|
||||
// Block kinds:
|
||||
// nav — thin top bar stand-in
|
||||
// bar — a rounded block; `size` controls height (sm/md/lg/xl);
|
||||
// `highlight: true` marks it as the ad slot, `label` names it
|
||||
// filters — a row of small pill shapes
|
||||
// grid — a row of small equal boxes (card grid stand-in)
|
||||
// list — a couple of thin stacked rows (table/list stand-in)
|
||||
// row — a flex row of `columns`, each rendered like a `bar`
|
||||
//
|
||||
// `overlay` (optional, top-level) renders the base `blocks` dimmed with a
|
||||
// centered floating highlighted box on top — used for popups.
|
||||
|
||||
export const PLACEMENT_LAYOUTS = {
|
||||
"dashboard.hero": {
|
||||
blocks: [
|
||||
{ kind: "nav" },
|
||||
{ kind: "bar", size: "xl", highlight: true, label: "Hero" },
|
||||
{ kind: "list", label: "My Groups" },
|
||||
{ kind: "grid", label: "Courses" },
|
||||
],
|
||||
},
|
||||
"dashboard.popup": {
|
||||
blocks: [
|
||||
{ kind: "nav" },
|
||||
{ kind: "bar", size: "xl" },
|
||||
{ kind: "list", label: "My Groups" },
|
||||
{ kind: "grid", label: "Courses" },
|
||||
],
|
||||
overlay: { label: "Popup" },
|
||||
},
|
||||
"course_list.banner": {
|
||||
blocks: [
|
||||
{ kind: "nav" },
|
||||
{ kind: "filters" },
|
||||
{ kind: "bar", size: "md", highlight: true, label: "Banner" },
|
||||
{ kind: "grid", label: "Course cards" },
|
||||
],
|
||||
},
|
||||
"course_details.banner": {
|
||||
blocks: [
|
||||
{ kind: "nav" },
|
||||
{ kind: "bar", size: "lg", label: "Course hero" },
|
||||
{ kind: "bar", size: "sm", highlight: true, label: "Banner" },
|
||||
{ kind: "row", columns: [
|
||||
{ label: "Course content", width: "flex-1" },
|
||||
{ label: "Sidebar", width: "w-1/4" },
|
||||
] },
|
||||
],
|
||||
},
|
||||
"course_details.sidebar": {
|
||||
blocks: [
|
||||
{ kind: "nav" },
|
||||
{ kind: "bar", size: "lg", label: "Course hero" },
|
||||
{ kind: "bar", size: "sm", label: "Banner" },
|
||||
{ kind: "row", columns: [
|
||||
{ label: "Course content", width: "flex-1" },
|
||||
{ label: "Sidebar", width: "w-1/4", highlight: true },
|
||||
] },
|
||||
],
|
||||
},
|
||||
"plans.banner": {
|
||||
blocks: [
|
||||
{ kind: "nav" },
|
||||
{ kind: "bar", size: "md", highlight: true, label: "Banner" },
|
||||
{ kind: "grid", label: "Plan cards" },
|
||||
],
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user