@fayz-ai/plugin-courses 0.2.4 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/dashboardWidgets.d.ts +8 -1
- package/dist/dashboardWidgets.d.ts.map +1 -1
- package/dist/index.cjs +46 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +49 -29
- package/dist/index.js.map +1 -1
- package/dist/pages/CoursesListPage.d.ts.map +1 -1
- package/dist/pages/FinancialPage.d.ts.map +1 -1
- package/package.json +9 -6
- package/src/dashboardWidgets.tsx +18 -5
- package/src/index.ts +58 -19
- package/src/pages/CoursesListPage.tsx +19 -6
- package/src/pages/FinancialPage.tsx +5 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @fayz-ai/plugin-courses
|
|
2
2
|
|
|
3
|
-
> **Status: experimental (incubating).** Not capability-complete — missing some or all of the capability bar (data-provider contract w/ supabase+mock pair, entity registries, settings, migrations; see `
|
|
3
|
+
> **Status: experimental (incubating).** Not capability-complete — missing some or all of the capability bar (data-provider contract w/ supabase+mock pair, entity registries, settings, migrations; see `docs/PLUGIN-PATTERNS.md`). Fine to explore in dogfoods; NOT ready for fresh installs or generated apps, and its API may change without notice.
|
|
4
4
|
|
|
5
5
|
> Drop a course-builder admin into any Fayz SaaS — courses, modules, lessons, enrollments.
|
|
6
6
|
|
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
import type { DashboardWidgetDef } from '@fayz-ai/core';
|
|
2
|
-
|
|
2
|
+
/** Commerce KPIs are contributed only for the modules the host opted into —
|
|
3
|
+
* a host embedding courses as a lightweight members feature must not get
|
|
4
|
+
* revenue/MRR cards broadcast onto its home dashboard (FAY-1247 rule). */
|
|
5
|
+
export declare function createCoursesDashboardWidgets(modules?: {
|
|
6
|
+
sales?: boolean;
|
|
7
|
+
subscriptions?: boolean;
|
|
8
|
+
financial?: boolean;
|
|
9
|
+
}): DashboardWidgetDef[];
|
|
3
10
|
//# sourceMappingURL=dashboardWidgets.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dashboardWidgets.d.ts","sourceRoot":"","sources":["../src/dashboardWidgets.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AA0CvD,wBAAgB,6BAA6B,
|
|
1
|
+
{"version":3,"file":"dashboardWidgets.d.ts","sourceRoot":"","sources":["../src/dashboardWidgets.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AA0CvD;;2EAE2E;AAC3E,wBAAgB,6BAA6B,CAC3C,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAA;CAAO,GAC9E,kBAAkB,EAAE,CAetB"}
|
package/dist/index.cjs
CHANGED
|
@@ -336,6 +336,8 @@ function CoursesListPage() {
|
|
|
336
336
|
const shellOwnsHeader = ui.useModuleLayout() === "tabs";
|
|
337
337
|
const [courses$1, setCourses] = React2__namespace.useState(null);
|
|
338
338
|
const [creating, setCreating] = React2__namespace.useState(false);
|
|
339
|
+
const guardCourses = saas.useLimitGuard("courses");
|
|
340
|
+
const can = saas.usePermissionOptional();
|
|
339
341
|
const load = React2__namespace.useCallback(async () => {
|
|
340
342
|
setCourses(await courses.getCoursesProvider().listCourses());
|
|
341
343
|
}, []);
|
|
@@ -343,24 +345,27 @@ function CoursesListPage() {
|
|
|
343
345
|
void load();
|
|
344
346
|
}, [load]);
|
|
345
347
|
async function onCreate() {
|
|
348
|
+
if (!can("courses", "create")) return;
|
|
349
|
+
if (await guardCourses() === "blocked") return;
|
|
346
350
|
setCreating(true);
|
|
347
351
|
try {
|
|
348
352
|
const course = await courses.getCoursesProvider().createCourse({
|
|
349
353
|
title: t("courses.editor.untitledCourse") || "Untitled course",
|
|
350
354
|
status: "draft"
|
|
351
355
|
});
|
|
356
|
+
saas.invalidateLimit("courses");
|
|
352
357
|
navigateTo(`/courses/${course.id}`);
|
|
353
358
|
} finally {
|
|
354
359
|
setCreating(false);
|
|
355
360
|
}
|
|
356
361
|
}
|
|
357
362
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto max-w-6xl p-6 md:p-8", children: [
|
|
358
|
-
shellOwnsHeader ? /* @__PURE__ */ jsxRuntime.jsx(ui.PageHeaderActions, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { onClick: onCreate, disabled: creating, children: t("courses.new") || "New course" }) }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-6 flex items-center justify-between", children: [
|
|
363
|
+
shellOwnsHeader ? /* @__PURE__ */ jsxRuntime.jsx(ui.PageHeaderActions, { children: /* @__PURE__ */ jsxRuntime.jsx(saas.PermissionGate, { feature: "courses", action: "create", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { onClick: onCreate, disabled: creating, children: t("courses.new") || "New course" }) }) }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-6 flex items-center justify-between", children: [
|
|
359
364
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
360
365
|
/* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-2xl font-bold text-foreground", children: t("courses.title") || "Courses" }),
|
|
361
366
|
courses$1 && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: (t("courses.count") || "{count} courses").replace("{count}", String(courses$1.length)) })
|
|
362
367
|
] }),
|
|
363
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { onClick: onCreate, disabled: creating, children: t("courses.new") || "New course" })
|
|
368
|
+
/* @__PURE__ */ jsxRuntime.jsx(saas.PermissionGate, { feature: "courses", action: "create", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { onClick: onCreate, disabled: creating, children: t("courses.new") || "New course" }) })
|
|
364
369
|
] }),
|
|
365
370
|
courses$1 && courses$1.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-xl border border-dashed border-border p-12 text-center text-muted-foreground", children: t("courses.empty") || "No courses yet. Create your first one." }),
|
|
366
371
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3", children: courses$1?.map((course) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
@@ -974,12 +979,14 @@ function FeesKpi() {
|
|
|
974
979
|
const s = useSummary();
|
|
975
980
|
return /* @__PURE__ */ jsxRuntime.jsx(ui.KpiCard, { label: t("courses.kpi.fees") || "Platform fees", icon: "Percent", loading: !s, value: s ? formatMoney(s.platformFeeTotal, s.currency) : void 0 });
|
|
976
981
|
}
|
|
977
|
-
function createCoursesDashboardWidgets() {
|
|
982
|
+
function createCoursesDashboardWidgets(modules = {}) {
|
|
978
983
|
return [
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
984
|
+
...modules.sales ? [
|
|
985
|
+
ui.defineKpiWidget({ id: "courses.kpi.revenue", title: "courses.kpi.revenue", domain: "courses", defaultOrder: 0, component: RevenueKpi }),
|
|
986
|
+
ui.defineKpiWidget({ id: "courses.kpi.sales", title: "courses.kpi.sales", domain: "courses", defaultOrder: 1, component: SalesKpi })
|
|
987
|
+
] : [],
|
|
988
|
+
...modules.subscriptions ? [ui.defineKpiWidget({ id: "courses.kpi.mrr", title: "courses.kpi.mrr", domain: "courses", defaultOrder: 2, component: MrrKpi })] : [],
|
|
989
|
+
...modules.financial ? [ui.defineKpiWidget({ id: "courses.kpi.fees", title: "courses.kpi.fees", domain: "courses", defaultOrder: 3, component: FeesKpi })] : []
|
|
983
990
|
];
|
|
984
991
|
}
|
|
985
992
|
function createStripeConnector() {
|
|
@@ -1019,6 +1026,27 @@ function createCoursesPlugin(options) {
|
|
|
1019
1026
|
}
|
|
1020
1027
|
const base = options?.navPosition ?? 1;
|
|
1021
1028
|
const section = options?.navSection ?? "main";
|
|
1029
|
+
const modules = options?.modules ?? {};
|
|
1030
|
+
const labels = options?.labels ?? {};
|
|
1031
|
+
const navigation = [
|
|
1032
|
+
{ section, position: base, label: options?.navLabel ?? "Courses", route: "/courses", icon: "BookOpen", permission: read("courses") },
|
|
1033
|
+
...modules.membersArea ? [{ section, position: base + 1, label: labels.membersArea ?? "Members area", route: "/courses/members", icon: "GraduationCap", permission: read("courses") }] : [],
|
|
1034
|
+
...modules.sales ? [{ section, position: base + 2, label: labels.sales ?? "Sales", route: "/courses/sales", icon: "TrendingUp", permission: read("courses.sales") }] : [],
|
|
1035
|
+
...modules.subscriptions ? [{ section, position: base + 3, label: labels.subscriptions ?? "Subscriptions", route: "/courses/subscriptions", icon: "Repeat", permission: read("courses.sales") }] : [],
|
|
1036
|
+
...modules.financial ? [{ section, position: base + 4, label: labels.financial ?? "Financial", route: "/courses/financial", icon: "CreditCard", permission: read("courses.finance") }] : [],
|
|
1037
|
+
...modules.reports ? [{ section, position: base + 5, label: labels.reports ?? "Reports", route: "/courses/reports", icon: "BarChart3", permission: read("courses.sales") }] : []
|
|
1038
|
+
];
|
|
1039
|
+
const routes = [
|
|
1040
|
+
// Static segments outscore the `:id` param in the saas router, so these
|
|
1041
|
+
// resolve before the editor route (AdminShell sorts by routeScore).
|
|
1042
|
+
{ path: "/courses", component: asComp(CoursesListPage), permission: read("courses") },
|
|
1043
|
+
...modules.membersArea ? [{ path: "/courses/members", component: asComp(MembersAreaPage), permission: read("courses") }] : [],
|
|
1044
|
+
...modules.sales ? [{ path: "/courses/sales", component: asComp(SalesPage), permission: read("courses.sales") }] : [],
|
|
1045
|
+
...modules.subscriptions ? [{ path: "/courses/subscriptions", component: asComp(SubscriptionsPage), permission: read("courses.sales") }] : [],
|
|
1046
|
+
...modules.financial ? [{ path: "/courses/financial", component: asComp(FinancialPage), permission: read("courses.finance") }] : [],
|
|
1047
|
+
...modules.reports ? [{ path: "/courses/reports", component: asComp(ReportsPage), permission: read("courses.sales") }] : [],
|
|
1048
|
+
{ path: "/courses/:id", component: asComp(CourseEditorPage) }
|
|
1049
|
+
];
|
|
1022
1050
|
return {
|
|
1023
1051
|
id: "courses",
|
|
1024
1052
|
name: options?.navLabel ?? "Courses",
|
|
@@ -1026,6 +1054,10 @@ function createCoursesPlugin(options) {
|
|
|
1026
1054
|
version: "1.0.0",
|
|
1027
1055
|
scope: options?.scope ?? "vertical",
|
|
1028
1056
|
verticalId: options?.verticalId,
|
|
1057
|
+
// LMS is its own business model with opt-in commerce modules
|
|
1058
|
+
// (members/sales/subscriptions — see modules.* below) — targets the
|
|
1059
|
+
// saas/admin Panel world rather than the ecommerce storefront.
|
|
1060
|
+
scaffolds: ["saas"],
|
|
1029
1061
|
defaultEnabled: true,
|
|
1030
1062
|
dependencies: [],
|
|
1031
1063
|
declaredFeatures: [
|
|
@@ -1033,27 +1065,15 @@ function createCoursesPlugin(options) {
|
|
|
1033
1065
|
{ id: "courses.sales", label: "Sales", group: "Courses" },
|
|
1034
1066
|
{ id: "courses.finance", label: "Financial", group: "Courses" }
|
|
1035
1067
|
],
|
|
1036
|
-
|
|
1037
|
-
{
|
|
1038
|
-
{ section, position: base + 1, label: "Members area", route: "/courses/members", icon: "GraduationCap", permission: read("courses") },
|
|
1039
|
-
{ section, position: base + 2, label: "Sales", route: "/courses/sales", icon: "TrendingUp", permission: read("courses.sales") },
|
|
1040
|
-
{ section, position: base + 3, label: "Subscriptions", route: "/courses/subscriptions", icon: "Repeat", permission: read("courses.sales") },
|
|
1041
|
-
{ section, position: base + 4, label: "Financial", route: "/courses/financial", icon: "CreditCard", permission: read("courses.finance") },
|
|
1042
|
-
{ section, position: base + 5, label: "Reports", route: "/courses/reports", icon: "BarChart3", permission: read("courses.sales") }
|
|
1043
|
-
],
|
|
1044
|
-
routes: [
|
|
1045
|
-
// Static segments outscore the `:id` param in the saas router, so these
|
|
1046
|
-
// resolve before the editor route (AdminShell sorts by routeScore).
|
|
1047
|
-
{ path: "/courses", component: asComp(CoursesListPage), permission: read("courses") },
|
|
1048
|
-
{ path: "/courses/members", component: asComp(MembersAreaPage), permission: read("courses") },
|
|
1049
|
-
{ path: "/courses/sales", component: asComp(SalesPage), permission: read("courses.sales") },
|
|
1050
|
-
{ path: "/courses/subscriptions", component: asComp(SubscriptionsPage), permission: read("courses.sales") },
|
|
1051
|
-
{ path: "/courses/financial", component: asComp(FinancialPage), permission: read("courses.finance") },
|
|
1052
|
-
{ path: "/courses/reports", component: asComp(ReportsPage), permission: read("courses.sales") },
|
|
1053
|
-
{ path: "/courses/:id", component: asComp(CourseEditorPage) }
|
|
1068
|
+
declaredLimits: [
|
|
1069
|
+
{ key: "courses", label: "Courses", table: "plg_courses_courses" }
|
|
1054
1070
|
],
|
|
1071
|
+
navigation,
|
|
1072
|
+
routes,
|
|
1055
1073
|
widgets: [],
|
|
1056
|
-
|
|
1074
|
+
// Commerce KPIs on the shared 'home' surface follow the same opt-in rule:
|
|
1075
|
+
// no module enabled → no widgets broadcast into a host's dashboard.
|
|
1076
|
+
dashboardWidgets: createCoursesDashboardWidgets(modules),
|
|
1057
1077
|
connectors: [createStripeConnector()],
|
|
1058
1078
|
settings: [
|
|
1059
1079
|
{ id: "courses", label: "Courses", icon: "BookOpen", component: asComp(CoursesSettingsTab), order: 20, permission: read("courses") }
|