@fayz-ai/plugin-courses 0.8.1 → 0.8.4

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.
Files changed (41) hide show
  1. package/dist/components/CommerceUI.d.ts +31 -1
  2. package/dist/components/CommerceUI.d.ts.map +1 -1
  3. package/dist/index.cjs +1237 -301
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +1240 -304
  7. package/dist/index.js.map +1 -1
  8. package/dist/locales/en.d.ts.map +1 -1
  9. package/dist/locales/pt-BR.d.ts.map +1 -1
  10. package/dist/pages/CourseEditorPage.d.ts.map +1 -1
  11. package/dist/pages/CoursesListPage.d.ts.map +1 -1
  12. package/dist/pages/SalesPage.d.ts.map +1 -1
  13. package/dist/public/CheckoutScreen.d.ts +17 -0
  14. package/dist/public/CheckoutScreen.d.ts.map +1 -0
  15. package/dist/public/createPublicCheckoutPlugin.d.ts +27 -0
  16. package/dist/public/createPublicCheckoutPlugin.d.ts.map +1 -0
  17. package/dist/public/data.d.ts +28 -0
  18. package/dist/public/data.d.ts.map +1 -0
  19. package/dist/public/data.supabase.d.ts +7 -0
  20. package/dist/public/data.supabase.d.ts.map +1 -0
  21. package/dist/public/index.d.ts +9 -0
  22. package/dist/public/index.d.ts.map +1 -0
  23. package/dist/views/CourseOrderDetailView.d.ts +10 -0
  24. package/dist/views/CourseOrderDetailView.d.ts.map +1 -0
  25. package/package.json +14 -5
  26. package/src/components/CommerceUI.tsx +78 -2
  27. package/src/index.ts +6 -3
  28. package/src/locales/en.ts +73 -12
  29. package/src/locales/pt-BR.ts +73 -12
  30. package/src/pages/CourseEditorPage.tsx +990 -177
  31. package/src/pages/CoursesListPage.tsx +20 -1
  32. package/src/pages/SalesPage.tsx +49 -20
  33. package/src/public/CheckoutScreen.tsx +279 -0
  34. package/src/public/createPublicCheckoutPlugin.tsx +88 -0
  35. package/src/public/data.supabase.ts +63 -0
  36. package/src/public/data.ts +89 -0
  37. package/src/public/index.ts +26 -0
  38. package/src/views/CourseOrderDetailView.tsx +132 -0
  39. package/dist/pages/MembersAreaPage.d.ts +0 -6
  40. package/dist/pages/MembersAreaPage.d.ts.map +0 -1
  41. package/src/pages/MembersAreaPage.tsx +0 -60
@@ -1 +1 @@
1
- {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/locales/en.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CA6IrC,CAAA"}
1
+ {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/locales/en.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CA0MrC,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"pt-BR.d.ts","sourceRoot":"","sources":["../../src/locales/pt-BR.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CA6IvC,CAAA"}
1
+ {"version":3,"file":"pt-BR.d.ts","sourceRoot":"","sources":["../../src/locales/pt-BR.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CA0MvC,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"CourseEditorPage.d.ts","sourceRoot":"","sources":["../../src/pages/CourseEditorPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAgB9B,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,EAAE,EAAE;IAAE,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,qBA0RvD;yBA1Re,gBAAgB"}
1
+ {"version":3,"file":"CourseEditorPage.d.ts","sourceRoot":"","sources":["../../src/pages/CourseEditorPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAqF9B,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,EAAE,EAAE;IAAE,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,qBAyiBvD;yBAziBe,gBAAgB"}
@@ -1 +1 @@
1
- {"version":3,"file":"CoursesListPage.d.ts","sourceRoot":"","sources":["../../src/pages/CoursesListPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAa9B,wBAAgB,eAAe,sBAuG9B;yBAvGe,eAAe"}
1
+ {"version":3,"file":"CoursesListPage.d.ts","sourceRoot":"","sources":["../../src/pages/CoursesListPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAa9B,wBAAgB,eAAe,sBAyG9B;yBAzGe,eAAe"}
@@ -1 +1 @@
1
- {"version":3,"file":"SalesPage.d.ts","sourceRoot":"","sources":["../../src/pages/SalesPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAc9B,wBAAgB,SAAS,sBAyCxB;yBAzCe,SAAS"}
1
+ {"version":3,"file":"SalesPage.d.ts","sourceRoot":"","sources":["../../src/pages/SalesPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAmB9B,wBAAgB,SAAS,sBAiExB;yBAjEe,SAAS"}
@@ -0,0 +1,17 @@
1
+ import { type FC } from 'react';
2
+ import type { PublicCheckoutDataProvider } from './data';
3
+ export interface CheckoutScreenProps {
4
+ slug: string;
5
+ provider: PublicCheckoutDataProvider;
6
+ /** Accent color (CSS) for buttons/highlights. Default emerald. */
7
+ accent?: string;
8
+ /** Where "acessar área de membros" points after success. Default '#/'. */
9
+ memberAreaHref?: string;
10
+ /** Fired after a successful order with the resulting order id + email. */
11
+ onSuccess?: (info: {
12
+ orderId: string;
13
+ email: string;
14
+ }) => void;
15
+ }
16
+ export declare const CheckoutScreen: FC<CheckoutScreenProps>;
17
+ //# sourceMappingURL=CheckoutScreen.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CheckoutScreen.d.ts","sourceRoot":"","sources":["../../src/public/CheckoutScreen.tsx"],"names":[],"mappings":"AAQA,OAAO,EAAgC,KAAK,EAAE,EAAkB,MAAM,OAAO,CAAA;AAG7E,OAAO,KAAK,EAAyB,0BAA0B,EAAE,MAAM,QAAQ,CAAA;AAE/E,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,0BAA0B,CAAA;IACpC,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,0EAA0E;IAC1E,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,0EAA0E;IAC1E,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;CAC/D;AAUD,eAAO,MAAM,cAAc,EAAE,EAAE,CAAC,mBAAmB,CAoPlD,CAAA"}
@@ -0,0 +1,27 @@
1
+ import { type FC } from 'react';
2
+ import type { PluginManifest, PluginScope, VerticalId } from '@fayz-ai/core';
3
+ import { type PublicCheckoutDataProvider } from './data';
4
+ import { type CheckoutScreenProps } from './CheckoutScreen';
5
+ export interface PublicCheckoutOptions {
6
+ /** Mount path for the public checkout route. Default '/checkout'. Slug is appended as ':slug'. */
7
+ basePath?: string;
8
+ /**
9
+ * Tenant this checkout sells for. REQUIRED when a Supabase client is
10
+ * configured (setGlobalSupabaseClient) — it scopes the catalog + the RPC.
11
+ */
12
+ tenantId?: string;
13
+ /** Accent color forwarded to CheckoutScreen. */
14
+ accent?: string;
15
+ /** Inject a custom provider (overrides the mock/Supabase resolver). */
16
+ dataProvider?: PublicCheckoutDataProvider;
17
+ scope?: PluginScope;
18
+ verticalId?: VerticalId;
19
+ }
20
+ export interface PublicCheckoutPlugin {
21
+ manifest: PluginManifest;
22
+ dataProvider: PublicCheckoutDataProvider;
23
+ /** CheckoutScreen bound to the resolved provider — pass `slug` (+ optional onSuccess). */
24
+ CheckoutScreen: FC<Omit<CheckoutScreenProps, 'provider'>>;
25
+ }
26
+ export declare function createPublicCheckoutPlugin(options?: PublicCheckoutOptions): PublicCheckoutPlugin;
27
+ //# sourceMappingURL=createPublicCheckoutPlugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createPublicCheckoutPlugin.d.ts","sourceRoot":"","sources":["../../src/public/createPublicCheckoutPlugin.tsx"],"names":[],"mappings":"AAUA,OAAO,EAAiB,KAAK,EAAE,EAAE,MAAM,OAAO,CAAA;AAC9C,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAC5E,OAAO,EAA8B,KAAK,0BAA0B,EAAE,MAAM,QAAQ,CAAA;AAEpF,OAAO,EAAkB,KAAK,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AAE3E,MAAM,WAAW,qBAAqB;IACpC,kGAAkG;IAClG,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,gDAAgD;IAChD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,uEAAuE;IACvE,YAAY,CAAC,EAAE,0BAA0B,CAAA;IACzC,KAAK,CAAC,EAAE,WAAW,CAAA;IACnB,UAAU,CAAC,EAAE,UAAU,CAAA;CACxB;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,cAAc,CAAA;IACxB,YAAY,EAAE,0BAA0B,CAAA;IACxC,0FAA0F;IAC1F,cAAc,EAAE,EAAE,CAAC,IAAI,CAAC,mBAAmB,EAAE,UAAU,CAAC,CAAC,CAAA;CAC1D;AAED,wBAAgB,0BAA0B,CAAC,OAAO,GAAE,qBAA0B,GAAG,oBAAoB,CAgDpG"}
@@ -0,0 +1,28 @@
1
+ import type { Course, Offer } from '@fayz-ai/courses';
2
+ export type CheckoutPaymentMethod = 'pix' | 'card';
3
+ export interface PlaceOrderInput {
4
+ email: string;
5
+ name?: string;
6
+ paymentMethod?: CheckoutPaymentMethod;
7
+ }
8
+ export interface PlaceOrderResult {
9
+ orderId: string;
10
+ }
11
+ export interface PublicCheckoutDataProvider {
12
+ /** Published course by slug (or id). Null when not found / not published. */
13
+ getCourseBySlug(slug: string): Promise<Course | null>;
14
+ /** The offer pre-selected on the checkout (isDefault, else the first). */
15
+ getDefaultOffer(courseId: string): Promise<Offer | null>;
16
+ /** Records the (simulated-paid) sale + enrollment. Returns the order id. */
17
+ placeOrder(offerId: string, input: PlaceOrderInput): Promise<PlaceOrderResult>;
18
+ }
19
+ export interface MockCheckoutOptions {
20
+ course?: Partial<Course>;
21
+ offer?: Partial<Offer>;
22
+ }
23
+ /**
24
+ * Loud mock: usable for a UI-only dev run with no backend. It fabricates a
25
+ * single course + offer and a fake order id so the flow renders end-to-end.
26
+ */
27
+ export declare function createMockCheckoutProvider(opts?: MockCheckoutOptions): PublicCheckoutDataProvider;
28
+ //# sourceMappingURL=data.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data.d.ts","sourceRoot":"","sources":["../../src/public/data.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;AAErD,MAAM,MAAM,qBAAqB,GAAG,KAAK,GAAG,MAAM,CAAA;AAElD,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,aAAa,CAAC,EAAE,qBAAqB,CAAA;CACtC;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,0BAA0B;IACzC,6EAA6E;IAC7E,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IACrD,0EAA0E;IAC1E,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,CAAA;IACxD,4EAA4E;IAC5E,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAA;CAC/E;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;IACxB,KAAK,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAA;CACvB;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,GAAE,mBAAwB,GAAG,0BAA0B,CA8CrG"}
@@ -0,0 +1,7 @@
1
+ import type { PublicCheckoutDataProvider } from './data';
2
+ export interface SupabaseCheckoutOptions {
3
+ /** Tenant this checkout sells for (RPC arg — required). */
4
+ tenantId: string;
5
+ }
6
+ export declare function createSupabaseCheckoutProvider(options: SupabaseCheckoutOptions): PublicCheckoutDataProvider;
7
+ //# sourceMappingURL=data.supabase.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data.supabase.d.ts","sourceRoot":"","sources":["../../src/public/data.supabase.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,0BAA0B,EAAmB,MAAM,QAAQ,CAAA;AAEzE,MAAM,WAAW,uBAAuB;IACtC,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAA;CACjB;AAYD,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,uBAAuB,GAAG,0BAA0B,CA8B3G"}
@@ -0,0 +1,9 @@
1
+ export { createPublicCheckoutPlugin } from './createPublicCheckoutPlugin';
2
+ export type { PublicCheckoutOptions, PublicCheckoutPlugin } from './createPublicCheckoutPlugin';
3
+ export { CheckoutScreen } from './CheckoutScreen';
4
+ export type { CheckoutScreenProps } from './CheckoutScreen';
5
+ export { createMockCheckoutProvider } from './data';
6
+ export { createSupabaseCheckoutProvider } from './data.supabase';
7
+ export type { SupabaseCheckoutOptions } from './data.supabase';
8
+ export type { PublicCheckoutDataProvider, PlaceOrderInput, PlaceOrderResult, CheckoutPaymentMethod, MockCheckoutOptions, } from './data';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/public/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAA;AACzE,YAAY,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAA;AAE/F,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AACjD,YAAY,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AAG3D,OAAO,EAAE,0BAA0B,EAAE,MAAM,QAAQ,CAAA;AACnD,OAAO,EAAE,8BAA8B,EAAE,MAAM,iBAAiB,CAAA;AAChE,YAAY,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAA;AAC9D,YAAY,EACV,0BAA0B,EAC1B,eAAe,EACf,gBAAgB,EAChB,qBAAqB,EACrB,mBAAmB,GACpB,MAAM,QAAQ,CAAA"}
@@ -0,0 +1,10 @@
1
+ import * as React from 'react';
2
+ /** Order detail (`/courses/sales/:id`). Loads the canonical order and renders the
3
+ * SHARED archetype OrderDetailView (@fayz-ai/saas) — same component the shop uses. */
4
+ export declare function CourseOrderDetailView({ id }: {
5
+ id?: string;
6
+ }): React.JSX.Element;
7
+ export declare namespace CourseOrderDetailView {
8
+ var displayName: string;
9
+ }
10
+ //# sourceMappingURL=CourseOrderDetailView.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CourseOrderDetailView.d.ts","sourceRoot":"","sources":["../../src/views/CourseOrderDetailView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AA+B9B;uFACuF;AACvF,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,EAAE,EAAE;IAAE,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,qBAiG5D;yBAjGe,qBAAqB"}
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "fayz": {
4
4
  "status": "preview"
5
5
  },
6
- "version": "0.8.1",
6
+ "version": "0.8.4",
7
7
  "description": "[experimental] Fayz SDK — courses admin management plugin",
8
8
  "type": "module",
9
9
  "main": "./dist/index.cjs",
@@ -15,6 +15,12 @@
15
15
  "types": "./dist/index.d.ts",
16
16
  "import": "./dist/index.js",
17
17
  "require": "./dist/index.cjs"
18
+ },
19
+ "./public": {
20
+ "source": "./src/public/index.ts",
21
+ "types": "./dist/public/index.d.ts",
22
+ "import": "./dist/public/index.js",
23
+ "require": "./dist/public/index.cjs"
18
24
  }
19
25
  },
20
26
  "files": [
@@ -28,10 +34,13 @@
28
34
  "dependencies": {
29
35
  "lucide-react": "^0.400.0",
30
36
  "@tanstack/react-table": "^8.20.0",
31
- "@fayz-ai/saas": "^0.8.2",
32
- "@fayz-ai/core": "^0.8.2",
33
- "@fayz-ai/ui": "^0.8.1",
34
- "@fayz-ai/courses": "^0.8.0"
37
+ "@dnd-kit/core": "^6.3.1",
38
+ "@dnd-kit/sortable": "^10.0.0",
39
+ "@dnd-kit/utilities": "^3.2.2",
40
+ "@fayz-ai/courses": "^0.8.1",
41
+ "@fayz-ai/saas": "^0.8.4",
42
+ "@fayz-ai/core": "^0.8.4",
43
+ "@fayz-ai/ui": "^0.8.4"
35
44
  },
36
45
  "devDependencies": {
37
46
  "@types/react": "^18.3.0",
@@ -47,6 +47,73 @@ export function TableEmpty({ children }: { children: React.ReactNode }) {
47
47
  )
48
48
  }
49
49
 
50
+ /**
51
+ * Standard empty state — an icon chip, a title, an optional description and a
52
+ * call-to-action. Use anywhere a list/section can be legitimately empty so the
53
+ * screen never shows a bare dashed box.
54
+ */
55
+ export function EmptyState({ icon, title, description, action }: {
56
+ icon?: React.ReactNode
57
+ title: string
58
+ description?: string
59
+ action?: React.ReactNode
60
+ }) {
61
+ return (
62
+ <div className="flex flex-col items-center justify-center rounded-xl border border-dashed border-border bg-card/40 px-6 py-12 text-center">
63
+ {icon && (
64
+ <div className="mb-3 flex h-11 w-11 items-center justify-center rounded-full bg-muted text-muted-foreground">
65
+ {icon}
66
+ </div>
67
+ )}
68
+ <p className="text-sm font-medium text-foreground">{title}</p>
69
+ {description && <p className="mt-1 max-w-sm text-sm text-muted-foreground">{description}</p>}
70
+ {action && <div className="mt-4">{action}</div>}
71
+ </div>
72
+ )
73
+ }
74
+
75
+ /**
76
+ * A muted, low-emphasis informational callout. Use to set honest expectations
77
+ * (e.g. "this applies to a feature that isn't shipped yet") without dressing it
78
+ * up as an interactive control. Wrap key phrases in <strong> for emphasis.
79
+ */
80
+ export function InfoNote({ icon, children, className }: {
81
+ icon?: React.ReactNode
82
+ children: React.ReactNode
83
+ className?: string
84
+ }) {
85
+ return (
86
+ <div className={'flex items-start gap-2.5 rounded-lg border border-border bg-muted/40 px-3.5 py-3 text-sm text-muted-foreground ' + (className ?? '')}>
87
+ {icon && <span className="mt-0.5 shrink-0 text-muted-foreground/80">{icon}</span>}
88
+ <div className="min-w-0 [&_strong]:font-medium [&_strong]:text-foreground">{children}</div>
89
+ </div>
90
+ )
91
+ }
92
+
93
+ /** A titled card section — the house pattern for grouping form fields. */
94
+ export function SectionCard({ title, description, actions, children, className }: {
95
+ title?: string
96
+ description?: string
97
+ actions?: React.ReactNode
98
+ children: React.ReactNode
99
+ className?: string
100
+ }) {
101
+ return (
102
+ <Card className={'p-5 md:p-6 ' + (className ?? '')}>
103
+ {(title || actions) && (
104
+ <div className="mb-4 flex items-start justify-between gap-4">
105
+ <div>
106
+ {title && <h2 className="text-sm font-semibold text-foreground">{title}</h2>}
107
+ {description && <p className="mt-0.5 text-sm text-muted-foreground">{description}</p>}
108
+ </div>
109
+ {actions}
110
+ </div>
111
+ )}
112
+ {children}
113
+ </Card>
114
+ )
115
+ }
116
+
50
117
  export interface SimpleColumn<T> {
51
118
  key: string
52
119
  header: string
@@ -59,11 +126,12 @@ export interface SimpleColumn<T> {
59
126
  * pages describe columns declaratively; this maps them to tanstack ColumnDef so
60
127
  * we reuse DataTable's sorting/skeleton/empty chrome instead of a bespoke table.
61
128
  */
62
- export function SimpleTable<T>({ columns, rows, empty, loading }: {
129
+ export function SimpleTable<T>({ columns, rows, empty, loading, onRowClick }: {
63
130
  columns: SimpleColumn<T>[]
64
131
  rows: T[]
65
132
  empty: string
66
133
  loading?: boolean
134
+ onRowClick?: (row: T) => void
67
135
  }) {
68
136
  const cols = React.useMemo<ColumnDef<T>[]>(
69
137
  () => columns.map((c) => ({
@@ -73,5 +141,13 @@ export function SimpleTable<T>({ columns, rows, empty, loading }: {
73
141
  })),
74
142
  [columns],
75
143
  )
76
- return <DataTable columns={cols} data={rows} emptyMessage={empty} loading={loading} />
144
+ return (
145
+ <DataTable
146
+ columns={cols}
147
+ data={rows}
148
+ emptyMessage={empty}
149
+ loading={loading}
150
+ onRowClick={onRowClick ? (row: T) => onRowClick(row) : undefined}
151
+ />
152
+ )
77
153
  }
package/src/index.ts CHANGED
@@ -12,10 +12,10 @@ import { coursesLocales } from './locales'
12
12
  import { CoursesListPage } from './pages/CoursesListPage'
13
13
  import { CourseEditorPage } from './pages/CourseEditorPage'
14
14
  import { SalesPage } from './pages/SalesPage'
15
+ import { CourseOrderDetailView } from './views/CourseOrderDetailView'
15
16
  import { SubscriptionsPage } from './pages/SubscriptionsPage'
16
17
  import { FinancialPage } from './pages/FinancialPage'
17
18
  import { ReportsPage } from './pages/ReportsPage'
18
- import { MembersAreaPage } from './pages/MembersAreaPage'
19
19
  import { CoursesSettingsTab } from './settings/CoursesSettings'
20
20
  import { createCoursesDashboardWidgets } from './dashboardWidgets'
21
21
  import { createStripeConnector } from './connectors/stripe'
@@ -85,7 +85,9 @@ export function createCoursesPlugin(options?: CoursesPluginOptions): PluginManif
85
85
  const labels = options?.labels ?? {}
86
86
  const navigation = [
87
87
  { section, position: base, label: options?.navLabel ?? 'Courses', route: '/courses', icon: 'BookOpen', permission: read('courses') },
88
- ...(modules.membersArea ? [{ section, position: base + 1, label: labels.membersArea ?? 'Members area', route: '/courses/members', icon: 'GraduationCap', permission: read('courses') }] : []),
88
+ // The members area is no longer a standalone nav entry — it lives inside each
89
+ // course's record (CourseEditorPage → "Alunos" tab). modules.membersArea is
90
+ // kept accepted-but-inert for backward compat with existing manifests.
89
91
  ...(modules.sales ? [{ section, position: base + 2, label: labels.sales ?? 'Sales', route: '/courses/sales', icon: 'TrendingUp', permission: read('courses.sales') }] : []),
90
92
  ...(modules.subscriptions ? [{ section, position: base + 3, label: labels.subscriptions ?? 'Subscriptions', route: '/courses/subscriptions', icon: 'Repeat', permission: read('courses.sales') }] : []),
91
93
  ...(modules.financial ? [{ section, position: base + 4, label: labels.financial ?? 'Financial', route: '/courses/financial', icon: 'CreditCard', permission: read('courses.finance') }] : []),
@@ -96,8 +98,9 @@ export function createCoursesPlugin(options?: CoursesPluginOptions): PluginManif
96
98
  // Static segments outscore the `:id` param in the saas router, so these
97
99
  // resolve before the editor route (AdminShell sorts by routeScore).
98
100
  { path: '/courses', component: asComp(CoursesListPage), permission: read('courses') },
99
- ...(modules.membersArea ? [{ path: '/courses/members', component: asComp(MembersAreaPage), permission: read('courses') }] : []),
100
101
  ...(modules.sales ? [{ path: '/courses/sales', component: asComp(SalesPage), permission: read('courses.sales') }] : []),
102
+ // Order detail — static `/courses/sales/:id` outscores `/courses/:id`.
103
+ ...(modules.sales ? [{ path: '/courses/sales/:id', component: asComp(CourseOrderDetailView), permission: read('courses.sales') }] : []),
101
104
  ...(modules.subscriptions ? [{ path: '/courses/subscriptions', component: asComp(SubscriptionsPage), permission: read('courses.sales') }] : []),
102
105
  ...(modules.financial ? [{ path: '/courses/financial', component: asComp(FinancialPage), permission: read('courses.finance') }] : []),
103
106
  ...(modules.reports ? [{ path: '/courses/reports', component: asComp(ReportsPage), permission: read('courses.sales') }] : []),
package/src/locales/en.ts CHANGED
@@ -19,14 +19,75 @@ export const en: Record<string, string> = {
19
19
  'courses.editor.fieldStatus': 'Status',
20
20
  'courses.editor.save': 'Save',
21
21
  'courses.editor.saved': 'Course saved',
22
+ 'courses.editor.saveFailed': 'Could not save the course',
23
+ 'courses.editor.unsaved': 'Unsaved changes',
24
+ // General tab
25
+ 'courses.editor.sectionBasics': 'Basics',
26
+ 'courses.editor.sectionThumbnail': 'Cover image',
27
+ 'courses.editor.sectionPricing': 'Pricing & status',
28
+ 'courses.editor.descriptionPlaceholder': 'What will students learn?',
29
+ 'courses.editor.fieldDelivery': 'Format',
30
+ 'courses.editor.deliveryHint': 'How this course is delivered.',
31
+ 'courses.editor.delivery.online': 'Online',
32
+ 'courses.editor.delivery.in_person': 'In person',
33
+ 'courses.editor.delivery.hybrid': 'Hybrid',
34
+ 'courses.editor.thumbnailHint': 'Shown on the storefront and in listings. 1280×720 recommended.',
35
+ 'courses.editor.thumbnailUpload': 'Upload image',
36
+ 'courses.editor.thumbnailUrl': 'Or paste an image URL',
37
+ 'courses.editor.thumbnailEmpty': 'No image',
38
+ 'courses.editor.thumbnailUploaded': 'Image uploaded — remember to save.',
39
+ 'courses.editor.uploading': 'Uploading…',
40
+ 'courses.editor.uploadFailed': 'Could not upload the image',
41
+ 'courses.editor.uploadUnavailable': 'Upload unavailable — paste an image URL instead.',
42
+ // Curriculum tab
22
43
  'courses.editor.addModule': 'Add module',
23
44
  'courses.editor.moduleTitle': 'Module title',
45
+ 'courses.editor.moduleAdded': 'Module added',
24
46
  'courses.editor.addLesson': 'Add lesson',
25
47
  'courses.editor.lessonTitle': 'Lesson title',
26
48
  'courses.editor.lessonVideo': 'Video URL (embed)',
27
49
  'courses.editor.lessonDuration': 'Duration (min)',
28
50
  'courses.editor.deleteModule': 'Delete module',
29
- 'courses.editor.deleteLesson': 'Remove',
51
+ 'courses.editor.deleteLesson': 'Remove lesson',
52
+ 'courses.editor.deleted': 'Removed',
53
+ 'courses.editor.autosaved': 'Changes are saved automatically.',
54
+ 'courses.editor.modulesEmpty': 'No modules yet',
55
+ 'courses.editor.modulesEmptyHint': 'Create a module to start organizing your lessons.',
56
+ 'courses.editor.lessonsEmpty': 'No lessons in this module yet.',
57
+ 'courses.editor.deleteModuleConfirm': 'Delete this module?',
58
+ 'courses.editor.deleteModuleConfirmBody': 'Its lessons will be removed too. This cannot be undone.',
59
+ 'courses.editor.deleteLessonConfirm': 'Remove this lesson?',
60
+ 'courses.editor.deleteLessonConfirmBody': 'This cannot be undone.',
61
+ 'courses.editor.confirmDelete': 'Delete',
62
+ 'courses.editor.cancel': 'Cancel',
63
+ // Curriculum tree builder
64
+ 'courses.editor.newModule': 'New module',
65
+ 'courses.editor.firstModule': 'Create first module',
66
+ 'courses.editor.newLesson': 'New lesson',
67
+ 'courses.editor.firstLesson': 'Add the first lesson',
68
+ 'courses.editor.moduleLabel': 'module',
69
+ 'courses.editor.moduleLabelPlural': 'modules',
70
+ 'courses.editor.lessonUnitOne': 'lesson',
71
+ 'courses.editor.lessonUnitOther': 'lessons',
72
+ 'courses.editor.noLessons': 'No lessons',
73
+ 'courses.editor.minShort': 'min',
74
+ 'courses.editor.hourShort': 'h',
75
+ 'courses.editor.dragToReorder': 'Drag to reorder',
76
+ 'courses.editor.expand': 'Expand',
77
+ 'courses.editor.collapse': 'Collapse',
78
+ 'courses.editor.editLesson': 'Edit lesson',
79
+ 'courses.editor.addVideo': 'Add video',
80
+ 'courses.editor.videoSet': 'Video',
81
+ 'courses.editor.videoUrlLabel': 'Video URL',
82
+ 'courses.editor.videoUrlPlaceholder': 'Paste the video link (YouTube, Vimeo…)',
83
+ 'courses.editor.reorderFailed': 'Could not reorder',
84
+ // Members area
85
+ 'courses.editor.membersHelp': 'Buyers are enrolled automatically through orders and get access to all of this course’s content.',
86
+ 'courses.editor.enrolledStudents': 'Enrolled students',
87
+ 'courses.editor.membersEmpty': 'No students yet',
88
+ 'courses.editor.membersEmptyHint': 'Buyers are enrolled here automatically after a paid order.',
89
+ 'courses.editor.accessUnlockedBy': 'Access is unlocked by',
90
+ 'courses.editor.membersNote': 'Buying any offer grants full access to this course’s content.',
30
91
  'courses.editor.untitledCourse': 'Untitled course',
31
92
  'courses.editor.untitledModule': 'New module',
32
93
  'courses.editor.untitledLesson': 'New lesson',
@@ -35,29 +96,29 @@ export const en: Record<string, string> = {
35
96
  'courses.editor.tab.curriculum': 'Curriculum',
36
97
  'courses.editor.tab.offers': 'Offers',
37
98
  'courses.editor.tab.members': 'Members area',
38
- 'courses.editor.tab.settings': 'Payment',
39
- 'courses.editor.tab.checkout': 'Checkout',
40
- 'courses.editor.tab.links': 'Links',
41
- 'courses.editor.membersHelp': 'Buyers of this course are added to its members area automatically. Access groups per offer:',
42
- 'courses.editor.defaultGroup': 'Default group',
43
- 'courses.editor.settingsHelp': 'Payment methods offered at checkout (Stripe).',
99
+ // Payment methods (used by the Courses plugin settings surface)
44
100
  'courses.editor.payCard': 'Credit card (up to 12x)',
45
101
  'courses.editor.payPix': 'Pix',
46
102
  'courses.editor.payBoleto': 'Boleto',
47
- 'courses.editor.settingsNote': 'Method availability depends on Stripe in your region; wired in Stage 3.',
48
- 'courses.editor.checkoutSoon': 'Custom checkout builder comes in a later milestone. A default checkout is generated per offer.',
49
- 'courses.editor.linksHelp': 'Shareable checkout links, one per offer.',
50
103
  // Offers
51
- 'courses.offers.help': 'Price points a customer can buy. Mark one default; order-bumps show on other checkouts.',
104
+ 'courses.offers.help': 'Price points for this course. Sales run through orders and the Financeiro module.',
52
105
  'courses.offers.name': 'Offer name',
53
106
  'courses.offers.price': 'Price',
107
+ 'courses.offers.kind': 'Type',
54
108
  'courses.offers.oneTime': 'One-time',
55
109
  'courses.offers.subscription': 'Subscription',
110
+ 'courses.offers.interval': 'Billing interval',
56
111
  'courses.offers.default': 'Default',
57
- 'courses.offers.bump': 'Bump',
112
+ 'courses.offers.bump': 'Order bump',
58
113
  'courses.offers.remove': 'Remove',
59
114
  'courses.offers.add': 'Add offer',
115
+ 'courses.offers.added': 'Offer added',
116
+ 'courses.offers.removed': 'Offer removed',
60
117
  'courses.offers.newName': 'New offer',
118
+ 'courses.offers.empty': 'No offers yet',
119
+ 'courses.offers.emptyHint': 'Create an offer to sell this course.',
120
+ 'courses.offers.firstOffer': 'Create first offer',
121
+ 'courses.offers.checkoutFlagsNote': 'Apply once the public checkout ships (coming soon).',
61
122
  // Sales
62
123
  'courses.sales.title': 'Sales',
63
124
  'courses.sales.subtitle': 'Every transaction across your courses.',
@@ -19,14 +19,75 @@ export const ptBR: Record<string, string> = {
19
19
  'courses.editor.fieldStatus': 'Status',
20
20
  'courses.editor.save': 'Salvar',
21
21
  'courses.editor.saved': 'Curso salvo',
22
+ 'courses.editor.saveFailed': 'Não foi possível salvar o curso',
23
+ 'courses.editor.unsaved': 'Alterações não salvas',
24
+ // Geral tab
25
+ 'courses.editor.sectionBasics': 'Informações básicas',
26
+ 'courses.editor.sectionThumbnail': 'Imagem de capa',
27
+ 'courses.editor.sectionPricing': 'Preço e publicação',
28
+ 'courses.editor.descriptionPlaceholder': 'O que o aluno vai aprender?',
29
+ 'courses.editor.fieldDelivery': 'Formato',
30
+ 'courses.editor.deliveryHint': 'Como o curso é entregue.',
31
+ 'courses.editor.delivery.online': 'Online',
32
+ 'courses.editor.delivery.in_person': 'Presencial',
33
+ 'courses.editor.delivery.hybrid': 'Híbrido',
34
+ 'courses.editor.thumbnailHint': 'Aparece na loja e nas listagens. Recomendado 1280×720.',
35
+ 'courses.editor.thumbnailUpload': 'Enviar imagem',
36
+ 'courses.editor.thumbnailUrl': 'Ou cole uma URL de imagem',
37
+ 'courses.editor.thumbnailEmpty': 'Sem imagem',
38
+ 'courses.editor.thumbnailUploaded': 'Imagem enviada — lembre de salvar.',
39
+ 'courses.editor.uploading': 'Enviando…',
40
+ 'courses.editor.uploadFailed': 'Não foi possível enviar a imagem',
41
+ 'courses.editor.uploadUnavailable': 'Upload indisponível — cole uma URL de imagem.',
42
+ // Conteúdo tab
22
43
  'courses.editor.addModule': 'Adicionar módulo',
23
44
  'courses.editor.moduleTitle': 'Título do módulo',
45
+ 'courses.editor.moduleAdded': 'Módulo adicionado',
24
46
  'courses.editor.addLesson': 'Adicionar aula',
25
47
  'courses.editor.lessonTitle': 'Título da aula',
26
48
  'courses.editor.lessonVideo': 'URL do vídeo (embed)',
27
49
  'courses.editor.lessonDuration': 'Duração (min)',
28
50
  'courses.editor.deleteModule': 'Excluir módulo',
29
- 'courses.editor.deleteLesson': 'Remover',
51
+ 'courses.editor.deleteLesson': 'Remover aula',
52
+ 'courses.editor.deleted': 'Removido',
53
+ 'courses.editor.autosaved': 'As alterações são salvas automaticamente.',
54
+ 'courses.editor.modulesEmpty': 'Nenhum módulo ainda',
55
+ 'courses.editor.modulesEmptyHint': 'Crie um módulo para começar a organizar as aulas.',
56
+ 'courses.editor.lessonsEmpty': 'Nenhuma aula neste módulo ainda.',
57
+ 'courses.editor.deleteModuleConfirm': 'Excluir este módulo?',
58
+ 'courses.editor.deleteModuleConfirmBody': 'As aulas deste módulo também serão removidas. Isso não pode ser desfeito.',
59
+ 'courses.editor.deleteLessonConfirm': 'Remover esta aula?',
60
+ 'courses.editor.deleteLessonConfirmBody': 'Isso não pode ser desfeito.',
61
+ 'courses.editor.confirmDelete': 'Excluir',
62
+ 'courses.editor.cancel': 'Cancelar',
63
+ // Curriculum tree builder
64
+ 'courses.editor.newModule': 'Novo módulo',
65
+ 'courses.editor.firstModule': 'Criar primeiro módulo',
66
+ 'courses.editor.newLesson': 'Nova aula',
67
+ 'courses.editor.firstLesson': 'Adicione a primeira aula',
68
+ 'courses.editor.moduleLabel': 'módulo',
69
+ 'courses.editor.moduleLabelPlural': 'módulos',
70
+ 'courses.editor.lessonUnitOne': 'aula',
71
+ 'courses.editor.lessonUnitOther': 'aulas',
72
+ 'courses.editor.noLessons': 'Sem aulas',
73
+ 'courses.editor.minShort': 'min',
74
+ 'courses.editor.hourShort': 'h',
75
+ 'courses.editor.dragToReorder': 'Arraste para reordenar',
76
+ 'courses.editor.expand': 'Expandir',
77
+ 'courses.editor.collapse': 'Recolher',
78
+ 'courses.editor.editLesson': 'Editar aula',
79
+ 'courses.editor.addVideo': 'Adicionar vídeo',
80
+ 'courses.editor.videoSet': 'Vídeo',
81
+ 'courses.editor.videoUrlLabel': 'URL do vídeo',
82
+ 'courses.editor.videoUrlPlaceholder': 'Cole o link do vídeo (YouTube, Vimeo…)',
83
+ 'courses.editor.reorderFailed': 'Não foi possível reordenar',
84
+ // Área de membros
85
+ 'courses.editor.membersHelp': 'Quem compra este curso é matriculado automaticamente pelos pedidos e ganha acesso a todo o conteúdo.',
86
+ 'courses.editor.enrolledStudents': 'Alunos matriculados',
87
+ 'courses.editor.membersEmpty': 'Nenhum aluno ainda',
88
+ 'courses.editor.membersEmptyHint': 'Os compradores são matriculados aqui automaticamente após um pedido pago.',
89
+ 'courses.editor.accessUnlockedBy': 'Acesso liberado por',
90
+ 'courses.editor.membersNote': 'Comprar qualquer oferta libera o acesso completo ao conteúdo deste curso.',
30
91
  'courses.editor.untitledCourse': 'Curso sem título',
31
92
  'courses.editor.untitledModule': 'Novo módulo',
32
93
  'courses.editor.untitledLesson': 'Nova aula',
@@ -35,29 +96,29 @@ export const ptBR: Record<string, string> = {
35
96
  'courses.editor.tab.curriculum': 'Conteúdo',
36
97
  'courses.editor.tab.offers': 'Ofertas',
37
98
  'courses.editor.tab.members': 'Área de membros',
38
- 'courses.editor.tab.settings': 'Pagamento',
39
- 'courses.editor.tab.checkout': 'Checkout',
40
- 'courses.editor.tab.links': 'Links',
41
- 'courses.editor.membersHelp': 'Quem compra este curso entra automaticamente na área de membros. Grupos de acesso por oferta:',
42
- 'courses.editor.defaultGroup': 'Grupo padrão',
43
- 'courses.editor.settingsHelp': 'Métodos de pagamento no checkout (Stripe).',
99
+ // Métodos de pagamento (usados na tela de configurações do plugin de Cursos)
44
100
  'courses.editor.payCard': 'Cartão de crédito (até 12x)',
45
101
  'courses.editor.payPix': 'Pix',
46
102
  'courses.editor.payBoleto': 'Boleto',
47
- 'courses.editor.settingsNote': 'A disponibilidade de cada método depende do Stripe na sua região; ligado na Etapa 3.',
48
- 'courses.editor.checkoutSoon': 'O construtor de checkout chega numa etapa futura. Um checkout padrão é gerado por oferta.',
49
- 'courses.editor.linksHelp': 'Links de checkout compartilháveis, um por oferta.',
50
103
  // Offers
51
- 'courses.offers.help': 'Preços que o cliente pode comprar. Marque um padrão; order-bumps aparecem nos checkouts das outras.',
104
+ 'courses.offers.help': 'Preços deste curso. As vendas acontecem pelos pedidos e pelo Financeiro.',
52
105
  'courses.offers.name': 'Nome da oferta',
53
106
  'courses.offers.price': 'Preço',
107
+ 'courses.offers.kind': 'Tipo',
54
108
  'courses.offers.oneTime': 'Único',
55
109
  'courses.offers.subscription': 'Assinatura',
110
+ 'courses.offers.interval': 'Intervalo de cobrança',
56
111
  'courses.offers.default': 'Padrão',
57
- 'courses.offers.bump': 'Bump',
112
+ 'courses.offers.bump': 'Order bump',
58
113
  'courses.offers.remove': 'Remover',
59
114
  'courses.offers.add': 'Adicionar oferta',
115
+ 'courses.offers.added': 'Oferta adicionada',
116
+ 'courses.offers.removed': 'Oferta removida',
60
117
  'courses.offers.newName': 'Nova oferta',
118
+ 'courses.offers.empty': 'Nenhuma oferta ainda',
119
+ 'courses.offers.emptyHint': 'Crie uma oferta para vender este curso.',
120
+ 'courses.offers.firstOffer': 'Criar primeira oferta',
121
+ 'courses.offers.checkoutFlagsNote': 'Aplicam-se quando o checkout público entrar no ar (em breve).',
61
122
  // Sales
62
123
  'courses.sales.title': 'Vendas',
63
124
  'courses.sales.subtitle': 'Todas as transações dos seus cursos.',