@fayz-ai/plugin-courses 0.8.3 → 0.9.0-next.0

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.d.ts.map +1 -1
  4. package/dist/index.js +1240 -304
  5. package/dist/index.js.map +1 -1
  6. package/dist/locales/en.d.ts.map +1 -1
  7. package/dist/locales/pt-BR.d.ts.map +1 -1
  8. package/dist/pages/CourseEditorPage.d.ts.map +1 -1
  9. package/dist/pages/CoursesListPage.d.ts.map +1 -1
  10. package/dist/pages/SalesPage.d.ts.map +1 -1
  11. package/dist/public/CheckoutScreen.d.ts +17 -0
  12. package/dist/public/CheckoutScreen.d.ts.map +1 -0
  13. package/dist/public/createPublicCheckoutPlugin.d.ts +27 -0
  14. package/dist/public/createPublicCheckoutPlugin.d.ts.map +1 -0
  15. package/dist/public/data.d.ts +28 -0
  16. package/dist/public/data.d.ts.map +1 -0
  17. package/dist/public/data.supabase.d.ts +7 -0
  18. package/dist/public/data.supabase.d.ts.map +1 -0
  19. package/dist/public/index.d.ts +9 -0
  20. package/dist/public/index.d.ts.map +1 -0
  21. package/dist/views/CourseOrderDetailView.d.ts +10 -0
  22. package/dist/views/CourseOrderDetailView.d.ts.map +1 -0
  23. package/package.json +20 -11
  24. package/src/components/CommerceUI.tsx +78 -2
  25. package/src/index.ts +6 -3
  26. package/src/locales/en.ts +73 -12
  27. package/src/locales/pt-BR.ts +73 -12
  28. package/src/pages/CourseEditorPage.tsx +990 -177
  29. package/src/pages/CoursesListPage.tsx +20 -1
  30. package/src/pages/SalesPage.tsx +49 -20
  31. package/src/public/CheckoutScreen.tsx +279 -0
  32. package/src/public/createPublicCheckoutPlugin.tsx +88 -0
  33. package/src/public/data.supabase.ts +63 -0
  34. package/src/public/data.ts +89 -0
  35. package/src/public/index.ts +26 -0
  36. package/src/views/CourseOrderDetailView.tsx +132 -0
  37. package/dist/index.cjs +0 -1089
  38. package/dist/index.cjs.map +0 -1
  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
package/dist/index.cjs DELETED
@@ -1,1089 +0,0 @@
1
- 'use strict';
2
-
3
- var core = require('@fayz-ai/core');
4
- var courses = require('@fayz-ai/courses');
5
- var saas = require('@fayz-ai/saas');
6
- var React2 = require('react');
7
- var ui = require('@fayz-ai/ui');
8
- var jsxRuntime = require('react/jsx-runtime');
9
-
10
- function _interopNamespace(e) {
11
- if (e && e.__esModule) return e;
12
- var n = Object.create(null);
13
- if (e) {
14
- Object.keys(e).forEach(function (k) {
15
- if (k !== 'default') {
16
- var d = Object.getOwnPropertyDescriptor(e, k);
17
- Object.defineProperty(n, k, d.get ? d : {
18
- enumerable: true,
19
- get: function () { return e[k]; }
20
- });
21
- }
22
- });
23
- }
24
- n.default = e;
25
- return Object.freeze(n);
26
- }
27
-
28
- var React2__namespace = /*#__PURE__*/_interopNamespace(React2);
29
-
30
- // src/index.ts
31
-
32
- // src/locales/en.ts
33
- var en = {
34
- "courses.nav": "Courses",
35
- "courses.title": "Courses",
36
- "courses.new": "New course",
37
- "courses.empty": "No courses yet. Create your first one.",
38
- "courses.count": "{count} courses",
39
- "courses.students": "students",
40
- "courses.lessons": "lessons",
41
- "courses.status.draft": "Draft",
42
- "courses.status.published": "Published",
43
- "courses.status.archived": "Archived",
44
- "courses.editor.back": "Back to courses",
45
- "courses.editor.details": "Details",
46
- "courses.editor.curriculum": "Curriculum",
47
- "courses.editor.fieldTitle": "Title",
48
- "courses.editor.fieldSubtitle": "Subtitle",
49
- "courses.editor.fieldDescription": "Description",
50
- "courses.editor.fieldPrice": "Price",
51
- "courses.editor.fieldStatus": "Status",
52
- "courses.editor.save": "Save",
53
- "courses.editor.saved": "Course saved",
54
- "courses.editor.addModule": "Add module",
55
- "courses.editor.moduleTitle": "Module title",
56
- "courses.editor.addLesson": "Add lesson",
57
- "courses.editor.lessonTitle": "Lesson title",
58
- "courses.editor.lessonVideo": "Video URL (embed)",
59
- "courses.editor.lessonDuration": "Duration (min)",
60
- "courses.editor.deleteModule": "Delete module",
61
- "courses.editor.deleteLesson": "Remove",
62
- "courses.editor.untitledCourse": "Untitled course",
63
- "courses.editor.untitledModule": "New module",
64
- "courses.editor.untitledLesson": "New lesson",
65
- // Editor tabs
66
- "courses.editor.tab.details": "General",
67
- "courses.editor.tab.curriculum": "Curriculum",
68
- "courses.editor.tab.offers": "Offers",
69
- "courses.editor.tab.members": "Members area",
70
- "courses.editor.tab.settings": "Payment",
71
- "courses.editor.tab.checkout": "Checkout",
72
- "courses.editor.tab.links": "Links",
73
- "courses.editor.membersHelp": "Buyers of this course are added to its members area automatically. Access groups per offer:",
74
- "courses.editor.defaultGroup": "Default group",
75
- "courses.editor.settingsHelp": "Payment methods offered at checkout (Stripe).",
76
- "courses.editor.payCard": "Credit card (up to 12x)",
77
- "courses.editor.payPix": "Pix",
78
- "courses.editor.payBoleto": "Boleto",
79
- "courses.editor.settingsNote": "Method availability depends on Stripe in your region; wired in Stage 3.",
80
- "courses.editor.checkoutSoon": "Custom checkout builder comes in a later milestone. A default checkout is generated per offer.",
81
- "courses.editor.linksHelp": "Shareable checkout links, one per offer.",
82
- // Offers
83
- "courses.offers.help": "Price points a customer can buy. Mark one default; order-bumps show on other checkouts.",
84
- "courses.offers.name": "Offer name",
85
- "courses.offers.price": "Price",
86
- "courses.offers.oneTime": "One-time",
87
- "courses.offers.subscription": "Subscription",
88
- "courses.offers.default": "Default",
89
- "courses.offers.bump": "Bump",
90
- "courses.offers.remove": "Remove",
91
- "courses.offers.add": "Add offer",
92
- "courses.offers.newName": "New offer",
93
- // Sales
94
- "courses.sales.title": "Sales",
95
- "courses.sales.subtitle": "Every transaction across your courses.",
96
- "courses.sales.date": "Date",
97
- "courses.sales.customer": "Customer",
98
- "courses.sales.method": "Method",
99
- "courses.sales.status": "Status",
100
- "courses.sales.fee": "Platform fee",
101
- "courses.sales.amount": "Amount",
102
- "courses.sales.count": "Sales",
103
- "courses.sales.revenue": "Revenue",
104
- "courses.sales.empty": "No sales yet.",
105
- "courses.financialStatus.paid": "Paid",
106
- "courses.financialStatus.pending": "Pending",
107
- "courses.financialStatus.refunded": "Refunded",
108
- "courses.financialStatus.chargeback": "Chargeback",
109
- // Subscriptions
110
- "courses.subs.title": "Subscriptions",
111
- "courses.subs.subtitle": "Recurring plans and MRR.",
112
- "courses.subs.start": "Start date",
113
- "courses.subs.customer": "Customer",
114
- "courses.subs.status": "Status",
115
- "courses.subs.value": "Net value",
116
- "courses.subs.active": "Active subscriptions",
117
- "courses.subs.mrr": "Monthly recurring revenue",
118
- "courses.subs.empty": "No subscriptions yet.",
119
- "courses.subStatus.active": "Active",
120
- "courses.subStatus.canceled": "Canceled",
121
- "courses.subStatus.past_due": "Past due",
122
- "courses.interval.month": "month",
123
- "courses.interval.year": "year",
124
- // Financial
125
- "courses.financial.title": "Financial settings",
126
- "courses.financial.subtitle": "Balance, payouts and your Stripe connection.",
127
- "courses.financial.available": "Available balance",
128
- "courses.financial.pending": "Pending balance",
129
- "courses.financial.revenue": "Total revenue",
130
- "courses.financial.feeCollected": "Platform fees",
131
- "courses.financial.refundRate": "refund rate",
132
- "courses.financial.payouts": "Payouts",
133
- "courses.financial.noPayouts": "No payouts yet.",
134
- "courses.financial.date": "Date",
135
- "courses.financial.amount": "Amount",
136
- "courses.financial.status": "Status",
137
- "courses.financial.connect": "Connect Stripe",
138
- "courses.financial.connected": "Stripe connected",
139
- "courses.financial.notConnected": "Connect your Stripe account to receive payouts. The platform fee is applied automatically on each sale.",
140
- "courses.financial.platformFee": "Platform fee",
141
- "courses.financial.connectSoon": "Stripe Connect onboarding wires up in Stage 3.",
142
- "courses.payoutStatus.paid": "Paid",
143
- "courses.payoutStatus.pending": "Pending",
144
- "courses.payoutStatus.failed": "Failed",
145
- // Reports
146
- "courses.reports.title": "Reports",
147
- "courses.reports.subtitle": "Revenue by course.",
148
- "courses.reports.byCourse": "Revenue by course",
149
- "courses.reports.course": "Course",
150
- "courses.reports.sales": "Sales",
151
- "courses.reports.revenue": "Revenue",
152
- "courses.reports.empty": "No revenue yet.",
153
- // Members area
154
- "courses.membersArea.title": "Members area",
155
- "courses.membersArea.subtitle": "One area per course; students land here after purchase.",
156
- "courses.membersArea.name": "Members area",
157
- "courses.membersArea.status": "Status",
158
- "courses.membersArea.students": "Students",
159
- "courses.membersArea.empty": "No members areas yet.",
160
- // Settings
161
- "courses.settings.title": "Course settings",
162
- "courses.settings.subtitle": "Payments, currency and checkout methods",
163
- "courses.settings.payments": "Payments",
164
- "courses.settings.paymentsHelp": "Connect Stripe in the Integrations tab; the platform fee applies automatically.",
165
- "courses.settings.currency": "Default currency",
166
- "courses.settings.methods": "Payment methods",
167
- // Dashboard KPIs
168
- "courses.kpi.revenue": "Total revenue",
169
- "courses.kpi.sales": "Sales",
170
- "courses.kpi.refunds": "refunds",
171
- "courses.kpi.mrr": "Recurring revenue (MRR)",
172
- "courses.kpi.activeSubs": "active",
173
- "courses.kpi.fees": "Platform fees"
174
- };
175
-
176
- // src/locales/pt-BR.ts
177
- var ptBR = {
178
- "courses.nav": "Cursos",
179
- "courses.title": "Cursos",
180
- "courses.new": "Novo curso",
181
- "courses.empty": "Nenhum curso ainda. Crie o primeiro.",
182
- "courses.count": "{count} cursos",
183
- "courses.students": "alunos",
184
- "courses.lessons": "aulas",
185
- "courses.status.draft": "Rascunho",
186
- "courses.status.published": "Publicado",
187
- "courses.status.archived": "Arquivado",
188
- "courses.editor.back": "Voltar aos cursos",
189
- "courses.editor.details": "Detalhes",
190
- "courses.editor.curriculum": "Conte\xFAdo",
191
- "courses.editor.fieldTitle": "T\xEDtulo",
192
- "courses.editor.fieldSubtitle": "Subt\xEDtulo",
193
- "courses.editor.fieldDescription": "Descri\xE7\xE3o",
194
- "courses.editor.fieldPrice": "Pre\xE7o",
195
- "courses.editor.fieldStatus": "Status",
196
- "courses.editor.save": "Salvar",
197
- "courses.editor.saved": "Curso salvo",
198
- "courses.editor.addModule": "Adicionar m\xF3dulo",
199
- "courses.editor.moduleTitle": "T\xEDtulo do m\xF3dulo",
200
- "courses.editor.addLesson": "Adicionar aula",
201
- "courses.editor.lessonTitle": "T\xEDtulo da aula",
202
- "courses.editor.lessonVideo": "URL do v\xEDdeo (embed)",
203
- "courses.editor.lessonDuration": "Dura\xE7\xE3o (min)",
204
- "courses.editor.deleteModule": "Excluir m\xF3dulo",
205
- "courses.editor.deleteLesson": "Remover",
206
- "courses.editor.untitledCourse": "Curso sem t\xEDtulo",
207
- "courses.editor.untitledModule": "Novo m\xF3dulo",
208
- "courses.editor.untitledLesson": "Nova aula",
209
- // Editor tabs
210
- "courses.editor.tab.details": "Geral",
211
- "courses.editor.tab.curriculum": "Conte\xFAdo",
212
- "courses.editor.tab.offers": "Ofertas",
213
- "courses.editor.tab.members": "\xC1rea de membros",
214
- "courses.editor.tab.settings": "Pagamento",
215
- "courses.editor.tab.checkout": "Checkout",
216
- "courses.editor.tab.links": "Links",
217
- "courses.editor.membersHelp": "Quem compra este curso entra automaticamente na \xE1rea de membros. Grupos de acesso por oferta:",
218
- "courses.editor.defaultGroup": "Grupo padr\xE3o",
219
- "courses.editor.settingsHelp": "M\xE9todos de pagamento no checkout (Stripe).",
220
- "courses.editor.payCard": "Cart\xE3o de cr\xE9dito (at\xE9 12x)",
221
- "courses.editor.payPix": "Pix",
222
- "courses.editor.payBoleto": "Boleto",
223
- "courses.editor.settingsNote": "A disponibilidade de cada m\xE9todo depende do Stripe na sua regi\xE3o; ligado na Etapa 3.",
224
- "courses.editor.checkoutSoon": "O construtor de checkout chega numa etapa futura. Um checkout padr\xE3o \xE9 gerado por oferta.",
225
- "courses.editor.linksHelp": "Links de checkout compartilh\xE1veis, um por oferta.",
226
- // Offers
227
- "courses.offers.help": "Pre\xE7os que o cliente pode comprar. Marque um padr\xE3o; order-bumps aparecem nos checkouts das outras.",
228
- "courses.offers.name": "Nome da oferta",
229
- "courses.offers.price": "Pre\xE7o",
230
- "courses.offers.oneTime": "\xDAnico",
231
- "courses.offers.subscription": "Assinatura",
232
- "courses.offers.default": "Padr\xE3o",
233
- "courses.offers.bump": "Bump",
234
- "courses.offers.remove": "Remover",
235
- "courses.offers.add": "Adicionar oferta",
236
- "courses.offers.newName": "Nova oferta",
237
- // Sales
238
- "courses.sales.title": "Vendas",
239
- "courses.sales.subtitle": "Todas as transa\xE7\xF5es dos seus cursos.",
240
- "courses.sales.date": "Data",
241
- "courses.sales.customer": "Cliente",
242
- "courses.sales.method": "M\xE9todo",
243
- "courses.sales.status": "Status",
244
- "courses.sales.fee": "Taxa da plataforma",
245
- "courses.sales.amount": "Valor",
246
- "courses.sales.count": "Vendas",
247
- "courses.sales.revenue": "Receita",
248
- "courses.sales.empty": "Nenhuma venda ainda.",
249
- "courses.financialStatus.paid": "Pago",
250
- "courses.financialStatus.pending": "Pendente",
251
- "courses.financialStatus.refunded": "Reembolsado",
252
- "courses.financialStatus.chargeback": "Chargeback",
253
- // Subscriptions
254
- "courses.subs.title": "Assinaturas",
255
- "courses.subs.subtitle": "Planos recorrentes e MRR.",
256
- "courses.subs.start": "In\xEDcio",
257
- "courses.subs.customer": "Cliente",
258
- "courses.subs.status": "Status",
259
- "courses.subs.value": "Valor l\xEDquido",
260
- "courses.subs.active": "Assinaturas ativas",
261
- "courses.subs.mrr": "Receita recorrente mensal",
262
- "courses.subs.empty": "Nenhuma assinatura ainda.",
263
- "courses.subStatus.active": "Ativa",
264
- "courses.subStatus.canceled": "Cancelada",
265
- "courses.subStatus.past_due": "Em atraso",
266
- "courses.interval.month": "m\xEAs",
267
- "courses.interval.year": "ano",
268
- // Financial
269
- "courses.financial.title": "Financeiro",
270
- "courses.financial.subtitle": "Saldo, saques e sua conex\xE3o com o Stripe.",
271
- "courses.financial.available": "Saldo dispon\xEDvel",
272
- "courses.financial.pending": "Saldo pendente",
273
- "courses.financial.revenue": "Receita total",
274
- "courses.financial.feeCollected": "Taxas da plataforma",
275
- "courses.financial.refundRate": "taxa de reembolso",
276
- "courses.financial.payouts": "Saques",
277
- "courses.financial.noPayouts": "Nenhum saque ainda.",
278
- "courses.financial.date": "Data",
279
- "courses.financial.amount": "Valor",
280
- "courses.financial.status": "Status",
281
- "courses.financial.connect": "Conectar Stripe",
282
- "courses.financial.connected": "Stripe conectado",
283
- "courses.financial.notConnected": "Conecte sua conta Stripe para receber os saques. A taxa da plataforma \xE9 aplicada automaticamente em cada venda.",
284
- "courses.financial.platformFee": "Taxa da plataforma",
285
- "courses.financial.connectSoon": "O onboarding do Stripe Connect \xE9 ligado na Etapa 3.",
286
- "courses.payoutStatus.paid": "Conclu\xEDdo",
287
- "courses.payoutStatus.pending": "Pendente",
288
- "courses.payoutStatus.failed": "Falhou",
289
- // Reports
290
- "courses.reports.title": "Relat\xF3rios",
291
- "courses.reports.subtitle": "Receita por curso.",
292
- "courses.reports.byCourse": "Receita por curso",
293
- "courses.reports.course": "Curso",
294
- "courses.reports.sales": "Vendas",
295
- "courses.reports.revenue": "Receita",
296
- "courses.reports.empty": "Nenhuma receita ainda.",
297
- // Members area
298
- "courses.membersArea.title": "\xC1rea de membros",
299
- "courses.membersArea.subtitle": "Uma \xE1rea por curso; o aluno entra ap\xF3s a compra.",
300
- "courses.membersArea.name": "\xC1rea de membros",
301
- "courses.membersArea.status": "Status",
302
- "courses.membersArea.students": "Alunos",
303
- "courses.membersArea.empty": "Nenhuma \xE1rea de membros ainda.",
304
- // Settings
305
- "courses.settings.title": "Configura\xE7\xF5es do curso",
306
- "courses.settings.subtitle": "Pagamentos, moeda e m\xE9todos de checkout",
307
- "courses.settings.payments": "Pagamentos",
308
- "courses.settings.paymentsHelp": "Conecte o Stripe na aba Integra\xE7\xF5es; a taxa da plataforma \xE9 aplicada automaticamente.",
309
- "courses.settings.currency": "Moeda padr\xE3o",
310
- "courses.settings.methods": "M\xE9todos de pagamento",
311
- // Dashboard KPIs
312
- "courses.kpi.revenue": "Receita total",
313
- "courses.kpi.sales": "Vendas",
314
- "courses.kpi.refunds": "reembolsos",
315
- "courses.kpi.mrr": "Receita recorrente (MRR)",
316
- "courses.kpi.activeSubs": "ativas",
317
- "courses.kpi.fees": "Taxas da plataforma"
318
- };
319
-
320
- // src/locales/index.ts
321
- var coursesLocales = {
322
- en,
323
- "pt-BR": ptBR
324
- };
325
- var adapter = core.hashRouterAdapter();
326
- function navigateTo(to) {
327
- adapter.navigate(to);
328
- }
329
- function statusVariant(status) {
330
- if (status === "published") return "success";
331
- if (status === "archived") return "outline";
332
- return "secondary";
333
- }
334
- function CoursesListPage() {
335
- const t = core.useTranslation();
336
- const shellOwnsHeader = ui.useModuleLayout() === "tabs";
337
- const [courses$1, setCourses] = React2__namespace.useState(null);
338
- const [creating, setCreating] = React2__namespace.useState(false);
339
- const guardCourses = saas.useLimitGuard("courses");
340
- const can = saas.usePermissionOptional();
341
- const load = React2__namespace.useCallback(async () => {
342
- setCourses(await courses.getCoursesProvider().listCourses());
343
- }, []);
344
- React2__namespace.useEffect(() => {
345
- void load();
346
- }, [load]);
347
- async function onCreate() {
348
- if (!can("courses", "create")) return;
349
- if (await guardCourses() === "blocked") return;
350
- setCreating(true);
351
- try {
352
- const course = await courses.getCoursesProvider().createCourse({
353
- title: t("courses.editor.untitledCourse") || "Untitled course",
354
- status: "draft"
355
- });
356
- saas.invalidateLimit("courses");
357
- navigateTo(`/courses/${course.id}`);
358
- } finally {
359
- setCreating(false);
360
- }
361
- }
362
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto max-w-6xl p-6 md:p-8", 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: [
364
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
365
- /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-2xl font-bold text-foreground", children: t("courses.title") || "Courses" }),
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)) })
367
- ] }),
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" }) })
369
- ] }),
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." }),
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(
372
- "button",
373
- {
374
- onClick: () => navigateTo(`/courses/${course.id}`),
375
- className: "group overflow-hidden rounded-xl border border-border bg-card text-left transition hover:border-primary hover:shadow-md",
376
- children: [
377
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "aspect-video w-full overflow-hidden bg-muted", children: course.thumbnailUrl && // eslint-disable-next-line @next/next/no-img-element
378
- /* @__PURE__ */ jsxRuntime.jsx("img", { src: course.thumbnailUrl, alt: "", className: "h-full w-full object-cover" }) }),
379
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2 p-4", children: [
380
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start justify-between gap-2", children: [
381
- /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "font-semibold text-foreground line-clamp-2", children: course.title }),
382
- /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { variant: statusVariant(course.status), children: t(`courses.status.${course.status}`) || course.status })
383
- ] }),
384
- course.subtitle && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground line-clamp-2", children: course.subtitle }),
385
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium text-foreground", children: course.price > 0 ? `${course.currency} ${course.price.toFixed(2)}` : "\u2014" })
386
- ] })
387
- ]
388
- },
389
- course.id
390
- )) })
391
- ] });
392
- }
393
- CoursesListPage.displayName = "CoursesListPage";
394
-
395
- // src/lib/format.ts
396
- function formatMoney(value, currency = "BRL") {
397
- try {
398
- return new Intl.NumberFormat("pt-BR", { style: "currency", currency }).format(value);
399
- } catch {
400
- return `${currency} ${value.toFixed(2)}`;
401
- }
402
- }
403
- function formatDate(iso) {
404
- try {
405
- return new Intl.DateTimeFormat("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric" }).format(new Date(iso));
406
- } catch {
407
- return iso.slice(0, 10);
408
- }
409
- }
410
- function formatPercent(fraction, digits = 0) {
411
- return `${(fraction * 100).toFixed(digits)}%`;
412
- }
413
- var STATUSES = ["draft", "published", "archived"];
414
- var TABS = ["details", "curriculum", "offers", "members", "settings", "checkout", "links"];
415
- function CourseEditorPage({ id }) {
416
- const t = core.useTranslation();
417
- const [tab, setTab] = React2__namespace.useState("details");
418
- const [course, setCourse] = React2__namespace.useState(null);
419
- const [modules, setModules] = React2__namespace.useState([]);
420
- const [lessons, setLessons] = React2__namespace.useState([]);
421
- const [offers, setOffers] = React2__namespace.useState([]);
422
- const reload = React2__namespace.useCallback(async () => {
423
- if (!id) return;
424
- const provider = courses.getCoursesProvider();
425
- const c = await provider.getCourse(id);
426
- setCourse(c);
427
- if (c) {
428
- const [m, l, o] = await Promise.all([
429
- provider.listModules(c.id),
430
- provider.listLessons(c.id),
431
- provider.listOffers(c.id)
432
- ]);
433
- setModules(m);
434
- setLessons(l);
435
- setOffers(o);
436
- }
437
- }, [id]);
438
- React2__namespace.useEffect(() => {
439
- void reload();
440
- }, [reload]);
441
- if (!course) {
442
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-8 text-muted-foreground", children: "\u2026" });
443
- }
444
- async function saveDetails() {
445
- await courses.getCoursesProvider().updateCourse(course.id, {
446
- title: course.title,
447
- subtitle: course.subtitle ?? void 0,
448
- description: course.description ?? void 0,
449
- price: course.price,
450
- status: course.status
451
- });
452
- ui.toast.success(t("courses.editor.saved") || "Course saved");
453
- }
454
- function patchCourse(patch) {
455
- setCourse((c) => c ? { ...c, ...patch } : c);
456
- }
457
- async function addModule() {
458
- await courses.getCoursesProvider().createModule({ courseId: course.id, title: t("courses.editor.untitledModule") || "New module" });
459
- await reload();
460
- }
461
- async function renameModule(moduleId, title) {
462
- await courses.getCoursesProvider().updateModule(moduleId, { title });
463
- }
464
- async function deleteModule(moduleId) {
465
- await courses.getCoursesProvider().deleteModule(moduleId);
466
- await reload();
467
- }
468
- async function addLesson(moduleId) {
469
- await courses.getCoursesProvider().createLesson({
470
- courseId: course.id,
471
- moduleId,
472
- title: t("courses.editor.untitledLesson") || "New lesson",
473
- videoUrl: "https://www.youtube.com/embed/dQw4w9WgXcQ"
474
- });
475
- await reload();
476
- }
477
- async function patchLesson(lessonId, patch) {
478
- await courses.getCoursesProvider().updateLesson(lessonId, {
479
- title: patch.title,
480
- videoUrl: patch.videoUrl,
481
- durationSec: patch.durationSec,
482
- description: patch.description ?? void 0
483
- });
484
- }
485
- async function deleteLesson(lessonId) {
486
- await courses.getCoursesProvider().deleteLesson(lessonId);
487
- await reload();
488
- }
489
- async function addOffer() {
490
- await courses.getCoursesProvider().createOffer({
491
- courseId: course.id,
492
- name: t("courses.offers.newName") || "New offer",
493
- price: course.price,
494
- kind: "one_time"
495
- });
496
- await reload();
497
- }
498
- async function patchOffer(offerId, patch) {
499
- await courses.getCoursesProvider().updateOffer(offerId, {
500
- name: patch.name,
501
- price: patch.price,
502
- kind: patch.kind,
503
- recurringInterval: patch.recurringInterval,
504
- isOrderBump: patch.isOrderBump,
505
- isDefault: patch.isDefault
506
- });
507
- await reload();
508
- }
509
- async function deleteOffer(offerId) {
510
- await courses.getCoursesProvider().deleteOffer(offerId);
511
- await reload();
512
- }
513
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto max-w-4xl p-6 md:p-8", children: [
514
- /* @__PURE__ */ jsxRuntime.jsxs("button", { onClick: () => navigateTo("/courses"), className: "mb-4 text-sm text-muted-foreground hover:text-foreground", children: [
515
- "\u2190 ",
516
- t("courses.editor.back") || "Back to courses"
517
- ] }),
518
- /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "mb-1 text-2xl font-bold text-foreground", children: course.title }),
519
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-6 mt-4 flex gap-1 overflow-x-auto border-b border-border", children: TABS.map((tk) => /* @__PURE__ */ jsxRuntime.jsx(
520
- "button",
521
- {
522
- onClick: () => setTab(tk),
523
- className: `-mb-px whitespace-nowrap border-b-2 px-3 py-2 text-sm font-medium ${tab === tk ? "border-primary text-foreground" : "border-transparent text-muted-foreground hover:text-foreground"}`,
524
- children: t(`courses.editor.tab.${tk}`) || tk
525
- },
526
- tk
527
- )) }),
528
- tab === "details" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "max-w-xl space-y-4", children: [
529
- /* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("courses.editor.fieldTitle") || "Title", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { value: course.title, onChange: (e) => patchCourse({ title: e.target.value }) }) }),
530
- /* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("courses.editor.fieldSubtitle") || "Subtitle", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { value: course.subtitle ?? "", onChange: (e) => patchCourse({ subtitle: e.target.value }) }) }),
531
- /* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("courses.editor.fieldDescription") || "Description", children: /* @__PURE__ */ jsxRuntime.jsx(
532
- "textarea",
533
- {
534
- className: "min-h-24 w-full rounded-md border border-input bg-background px-3 py-2 text-sm",
535
- value: course.description ?? "",
536
- onChange: (e) => patchCourse({ description: e.target.value })
537
- }
538
- ) }),
539
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-4", children: [
540
- /* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("courses.editor.fieldPrice") || "Price", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { type: "number", value: String(course.price), onChange: (e) => patchCourse({ price: Number(e.target.value) || 0 }) }) }),
541
- /* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("courses.editor.fieldStatus") || "Status", children: /* @__PURE__ */ jsxRuntime.jsx(
542
- "select",
543
- {
544
- className: "h-10 rounded-md border border-input bg-background px-3 text-sm",
545
- value: course.status,
546
- onChange: (e) => patchCourse({ status: e.target.value }),
547
- children: STATUSES.map((s) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: s, children: t(`courses.status.${s}`) || s }, s))
548
- }
549
- ) })
550
- ] }),
551
- /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { onClick: saveDetails, children: t("courses.editor.save") || "Save" })
552
- ] }),
553
- tab === "curriculum" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-6", children: [
554
- modules.map((mod) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-xl border border-border bg-card p-4", children: [
555
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-3 flex items-center gap-2", children: [
556
- /* @__PURE__ */ jsxRuntime.jsx(
557
- ui.Input,
558
- {
559
- className: "font-medium",
560
- defaultValue: mod.title,
561
- onBlur: (e) => renameModule(mod.id, e.target.value),
562
- "aria-label": t("courses.editor.moduleTitle") || "Module title"
563
- }
564
- ),
565
- /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "ghost", size: "sm", onClick: () => deleteModule(mod.id), children: t("courses.editor.deleteModule") || "Delete module" })
566
- ] }),
567
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
568
- lessons.filter((l) => l.moduleId === mod.id).map((lesson) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 gap-2 rounded-md border border-border p-3 md:grid-cols-[1fr_1fr_90px_auto]", children: [
569
- /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { defaultValue: lesson.title, placeholder: t("courses.editor.lessonTitle") || "Lesson title", onBlur: (e) => patchLesson(lesson.id, { title: e.target.value }) }),
570
- /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { defaultValue: lesson.videoUrl, placeholder: t("courses.editor.lessonVideo") || "Video URL (embed)", onBlur: (e) => patchLesson(lesson.id, { videoUrl: e.target.value }) }),
571
- /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { type: "number", defaultValue: String(Math.round(lesson.durationSec / 60)), onBlur: (e) => patchLesson(lesson.id, { durationSec: (Number(e.target.value) || 0) * 60 }), "aria-label": t("courses.editor.lessonDuration") || "Duration (min)" }),
572
- /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "ghost", size: "sm", onClick: () => deleteLesson(lesson.id), children: t("courses.editor.deleteLesson") || "Remove" })
573
- ] }, lesson.id)),
574
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { variant: "outline", size: "sm", onClick: () => addLesson(mod.id), children: [
575
- "+ ",
576
- t("courses.editor.addLesson") || "Add lesson"
577
- ] })
578
- ] })
579
- ] }, mod.id)),
580
- /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { onClick: addModule, children: t("courses.editor.addModule") || "Add module" })
581
- ] }),
582
- tab === "offers" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
583
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: t("courses.offers.help") || "Price points a customer can buy. Mark one default; order-bumps show on other checkouts." }),
584
- offers.map((offer) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 gap-2 rounded-xl border border-border bg-card p-3 md:grid-cols-[1fr_120px_130px_auto]", children: [
585
- /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { defaultValue: offer.name, onBlur: (e) => patchOffer(offer.id, { name: e.target.value }), "aria-label": t("courses.offers.name") || "Offer name" }),
586
- /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { type: "number", defaultValue: String(offer.price), onBlur: (e) => patchOffer(offer.id, { price: Number(e.target.value) || 0 }), "aria-label": t("courses.offers.price") || "Price" }),
587
- /* @__PURE__ */ jsxRuntime.jsxs(
588
- "select",
589
- {
590
- className: "h-10 rounded-md border border-input bg-background px-2 text-sm",
591
- value: offer.kind,
592
- onChange: (e) => patchOffer(offer.id, { kind: e.target.value, recurringInterval: e.target.value === "subscription" ? offer.recurringInterval ?? "month" : null }),
593
- children: [
594
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "one_time", children: t("courses.offers.oneTime") || "One-time" }),
595
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "subscription", children: t("courses.offers.subscription") || "Subscription" })
596
- ]
597
- }
598
- ),
599
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
600
- offer.isDefault && /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { variant: "secondary", children: t("courses.offers.default") || "Default" }),
601
- offer.isOrderBump && /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { variant: "outline", children: t("courses.offers.bump") || "Bump" }),
602
- /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "ghost", size: "sm", onClick: () => deleteOffer(offer.id), children: t("courses.offers.remove") || "Remove" })
603
- ] })
604
- ] }, offer.id)),
605
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { onClick: addOffer, children: [
606
- "+ ",
607
- t("courses.offers.add") || "Add offer"
608
- ] })
609
- ] }),
610
- tab === "members" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3 text-sm", children: [
611
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground", children: t("courses.editor.membersHelp") || "Buyers of this course are added to its members area automatically. Access groups per offer:" }),
612
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-xl border border-border bg-card divide-y divide-border", children: offers.map((offer) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-4 py-3", children: [
613
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: offer.name }),
614
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatMoney(offer.price, offer.currency) }),
615
- /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { variant: "secondary", children: t("courses.editor.defaultGroup") || "Default group" })
616
- ] }, offer.id)) })
617
- ] }),
618
- tab === "settings" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "max-w-xl space-y-4 text-sm", children: [
619
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground", children: t("courses.editor.settingsHelp") || "Payment methods offered at checkout (Stripe)." }),
620
- [
621
- ["card", t("courses.editor.payCard") || "Credit card (up to 12x)"],
622
- ["pix", t("courses.editor.payPix") || "Pix"],
623
- ["boleto", t("courses.editor.payBoleto") || "Boleto"]
624
- ].map(([k, label]) => /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex items-center gap-3 rounded-lg border border-border bg-card px-4 py-3", children: [
625
- /* @__PURE__ */ jsxRuntime.jsx("input", { type: "checkbox", defaultChecked: true, className: "h-4 w-4" }),
626
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-foreground", children: label })
627
- ] }, k)),
628
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: t("courses.editor.settingsNote") || "Method availability depends on Stripe in your region; wired in Stage 3." })
629
- ] }),
630
- tab === "checkout" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-xl border border-dashed border-border p-10 text-center text-sm text-muted-foreground", children: t("courses.editor.checkoutSoon") || "Custom checkout builder comes in a later milestone. A default checkout is generated per offer." }),
631
- tab === "links" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
632
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: t("courses.editor.linksHelp") || "Shareable checkout links, one per offer." }),
633
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-xl border border-border bg-card divide-y divide-border", children: offers.map((offer) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1 px-4 py-3 md:flex-row md:items-center md:justify-between", children: [
634
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: offer.name }),
635
- /* @__PURE__ */ jsxRuntime.jsxs("code", { className: "rounded bg-muted px-2 py-1 text-xs text-muted-foreground", children: [
636
- "/checkout/",
637
- course.slug,
638
- "?offer=",
639
- offer.id
640
- ] })
641
- ] }, offer.id)) })
642
- ] })
643
- ] });
644
- }
645
- CourseEditorPage.displayName = "CourseEditorPage";
646
- function Field({ label, children }) {
647
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1.5", children: [
648
- /* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-sm font-medium text-foreground", children: label }),
649
- children
650
- ] });
651
- }
652
- function PageContainer({ title, subtitle, actions, children }) {
653
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto max-w-6xl p-6 md:p-8", children: [
654
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-6 flex items-start justify-between gap-4", children: [
655
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
656
- /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-2xl font-bold text-foreground", children: title }),
657
- subtitle && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: subtitle })
658
- ] }),
659
- actions
660
- ] }),
661
- children
662
- ] });
663
- }
664
- function StatCard({ label, value, hint }) {
665
- return /* @__PURE__ */ jsxRuntime.jsxs(ui.Card, { className: "p-5", children: [
666
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: label }),
667
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-2xl font-bold text-foreground", children: value }),
668
- hint && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: hint })
669
- ] });
670
- }
671
- function StatGrid({ children }) {
672
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4", children });
673
- }
674
- function SimpleTable({ columns, rows, empty, loading }) {
675
- const cols = React2__namespace.useMemo(
676
- () => columns.map((c) => ({
677
- id: c.key,
678
- header: () => /* @__PURE__ */ jsxRuntime.jsx("div", { className: c.align === "right" ? "text-right" : "", children: c.header }),
679
- cell: ({ row }) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: c.align === "right" ? "text-right" : "", children: c.render(row.original) })
680
- })),
681
- [columns]
682
- );
683
- return /* @__PURE__ */ jsxRuntime.jsx(ui.DataTable, { columns: cols, data: rows, emptyMessage: empty, loading });
684
- }
685
- function statusVariant2(s) {
686
- if (s === "paid") return "success";
687
- if (s === "pending") return "secondary";
688
- if (s === "refunded" || s === "chargeback") return "destructive";
689
- return "outline";
690
- }
691
- function SalesPage() {
692
- const t = core.useTranslation();
693
- const [orders, setOrders] = React2__namespace.useState(null);
694
- React2__namespace.useEffect(() => {
695
- void courses.getCoursesProvider().listOrders().then(setOrders);
696
- }, []);
697
- const paid = (orders ?? []).filter((o) => o.financialStatus === "paid");
698
- const revenue = paid.reduce((s, o) => s + o.total, 0);
699
- const currency = orders?.[0]?.currency ?? "BRL";
700
- const columns = [
701
- { key: "date", header: t("courses.sales.date") || "Date", render: (o) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatDate(o.createdAt) }) },
702
- { key: "customer", header: t("courses.sales.customer") || "Customer", render: (o) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
703
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "font-medium text-foreground", children: o.customerName ?? "\u2014" }),
704
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-muted-foreground", children: o.customerEmail })
705
- ] }) },
706
- { key: "method", header: t("courses.sales.method") || "Method", render: (o) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "capitalize", children: o.paymentMethod ?? "\u2014" }) },
707
- { key: "status", header: t("courses.sales.status") || "Status", render: (o) => /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { variant: statusVariant2(o.financialStatus), children: t(`courses.financialStatus.${o.financialStatus}`) || o.financialStatus }) },
708
- { key: "fee", header: t("courses.sales.fee") || "Platform fee", align: "right", render: (o) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatMoney(o.platformFee, o.currency) }) },
709
- { key: "total", header: t("courses.sales.amount") || "Amount", align: "right", render: (o) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: formatMoney(o.total, o.currency) }) }
710
- ];
711
- return /* @__PURE__ */ jsxRuntime.jsxs(PageContainer, { title: t("courses.sales.title") || "Sales", subtitle: t("courses.sales.subtitle") || "Every transaction across your courses.", children: [
712
- /* @__PURE__ */ jsxRuntime.jsxs(StatGrid, { children: [
713
- /* @__PURE__ */ jsxRuntime.jsx(StatCard, { label: t("courses.sales.count") || "Sales", value: String(paid.length) }),
714
- /* @__PURE__ */ jsxRuntime.jsx(StatCard, { label: t("courses.sales.revenue") || "Revenue", value: formatMoney(revenue, currency) })
715
- ] }),
716
- /* @__PURE__ */ jsxRuntime.jsx(
717
- SimpleTable,
718
- {
719
- columns,
720
- rows: orders ?? [],
721
- empty: t("courses.sales.empty") || "No sales yet."
722
- }
723
- )
724
- ] });
725
- }
726
- SalesPage.displayName = "SalesPage";
727
- function statusVariant3(s) {
728
- if (s === "active") return "success";
729
- if (s === "past_due") return "secondary";
730
- return "destructive";
731
- }
732
- function SubscriptionsPage() {
733
- const t = core.useTranslation();
734
- const [subs, setSubs] = React2__namespace.useState(null);
735
- React2__namespace.useEffect(() => {
736
- void courses.getCoursesProvider().listSubscriptions().then(setSubs);
737
- }, []);
738
- const active = (subs ?? []).filter((s) => s.status === "active");
739
- const mrr = active.reduce((sum, s) => sum + (s.interval === "year" ? s.netValue / 12 : s.netValue), 0);
740
- const currency = subs?.[0]?.currency ?? "BRL";
741
- const columns = [
742
- { key: "start", header: t("courses.subs.start") || "Start date", render: (s) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatDate(s.startedAt) }) },
743
- { key: "customer", header: t("courses.subs.customer") || "Customer", render: (s) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
744
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "font-medium text-foreground", children: s.customerName ?? "\u2014" }),
745
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-muted-foreground", children: s.customerEmail })
746
- ] }) },
747
- { key: "status", header: t("courses.subs.status") || "Status", render: (s) => /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { variant: statusVariant3(s.status), children: t(`courses.subStatus.${s.status}`) || s.status }) },
748
- { key: "value", header: t("courses.subs.value") || "Net value", align: "right", render: (s) => /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-medium text-foreground", children: [
749
- formatMoney(s.netValue, s.currency),
750
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-muted-foreground", children: [
751
- " / ",
752
- t(`courses.interval.${s.interval}`) || s.interval
753
- ] })
754
- ] }) }
755
- ];
756
- return /* @__PURE__ */ jsxRuntime.jsxs(PageContainer, { title: t("courses.subs.title") || "Subscriptions", subtitle: t("courses.subs.subtitle") || "Recurring plans and MRR.", children: [
757
- /* @__PURE__ */ jsxRuntime.jsxs(StatGrid, { children: [
758
- /* @__PURE__ */ jsxRuntime.jsx(StatCard, { label: t("courses.subs.active") || "Active subscriptions", value: String(active.length) }),
759
- /* @__PURE__ */ jsxRuntime.jsx(StatCard, { label: t("courses.subs.mrr") || "Monthly recurring revenue", value: formatMoney(mrr, currency) })
760
- ] }),
761
- /* @__PURE__ */ jsxRuntime.jsx(
762
- SimpleTable,
763
- {
764
- columns,
765
- rows: subs ?? [],
766
- empty: t("courses.subs.empty") || "No subscriptions yet."
767
- }
768
- )
769
- ] });
770
- }
771
- SubscriptionsPage.displayName = "SubscriptionsPage";
772
- function FinancialPage() {
773
- const t = core.useTranslation();
774
- const [summary, setSummary] = React2__namespace.useState(null);
775
- const [payouts, setPayouts] = React2__namespace.useState(null);
776
- const [account, setAccount] = React2__namespace.useState(null);
777
- React2__namespace.useEffect(() => {
778
- const p = courses.getCoursesProvider();
779
- void p.getFinancialSummary().then(setSummary);
780
- void p.listPayouts().then(setPayouts);
781
- void p.getCreatorAccount().then(setAccount);
782
- }, []);
783
- const currency = summary?.currency ?? "BRL";
784
- const connected = !!account?.stripeAccountId && account.chargesEnabled;
785
- function connectStripe() {
786
- ui.toast.info(t("courses.financial.connectSoon") || "Stripe Connect onboarding wires up in Stage 3.");
787
- }
788
- const columns = [
789
- { key: "date", header: t("courses.financial.date") || "Date", render: (p) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatDate(p.createdAt) }) },
790
- { key: "amount", header: t("courses.financial.amount") || "Amount", align: "right", render: (p) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: formatMoney(p.amount, p.currency) }) },
791
- { key: "status", header: t("courses.financial.status") || "Status", align: "right", render: (p) => /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { variant: p.status === "paid" ? "success" : p.status === "pending" ? "secondary" : "destructive", children: t(`courses.payoutStatus.${p.status}`) || p.status }) }
792
- ];
793
- return /* @__PURE__ */ jsxRuntime.jsxs(
794
- PageContainer,
795
- {
796
- title: t("courses.financial.title") || "Financial settings",
797
- subtitle: t("courses.financial.subtitle") || "Balance, payouts and your Stripe connection.",
798
- actions: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: connected ? "outline" : "default", onClick: connectStripe, children: connected ? t("courses.financial.connected") || "Stripe connected" : t("courses.financial.connect") || "Connect Stripe" }),
799
- children: [
800
- !connected && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-6 rounded-xl border border-amber-500/40 bg-amber-500/10 p-4 text-sm text-foreground", children: [
801
- t("courses.financial.notConnected") || "Connect your Stripe account to receive payouts. The platform fee is applied automatically on each sale.",
802
- account && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ml-1 text-muted-foreground", children: [
803
- "(",
804
- t("courses.financial.platformFee") || "Platform fee",
805
- ": ",
806
- (account.platformFeeBps / 100).toFixed(2),
807
- "%)"
808
- ] })
809
- ] }),
810
- /* @__PURE__ */ jsxRuntime.jsxs(StatGrid, { children: [
811
- /* @__PURE__ */ jsxRuntime.jsx(StatCard, { label: t("courses.financial.available") || "Available balance", value: formatMoney(summary?.availableBalance ?? 0, currency) }),
812
- /* @__PURE__ */ jsxRuntime.jsx(StatCard, { label: t("courses.financial.pending") || "Pending balance", value: formatMoney(summary?.pendingBalance ?? 0, currency) }),
813
- /* @__PURE__ */ jsxRuntime.jsx(StatCard, { label: t("courses.financial.revenue") || "Total revenue", value: formatMoney(summary?.totalRevenue ?? 0, currency) }),
814
- /* @__PURE__ */ jsxRuntime.jsx(StatCard, { label: t("courses.financial.feeCollected") || "Platform fees", value: formatMoney(summary?.platformFeeTotal ?? 0, currency), hint: summary ? formatPercent(summary.refundRate, 1) + " " + (t("courses.financial.refundRate") || "refund rate") : void 0 })
815
- ] }),
816
- /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "mb-3 text-sm font-semibold text-foreground", children: t("courses.financial.payouts") || "Payouts" }),
817
- /* @__PURE__ */ jsxRuntime.jsx(
818
- SimpleTable,
819
- {
820
- columns,
821
- rows: payouts ?? [],
822
- empty: t("courses.financial.noPayouts") || "No payouts yet."
823
- }
824
- )
825
- ]
826
- }
827
- );
828
- }
829
- FinancialPage.displayName = "FinancialPage";
830
- function ReportsPage() {
831
- const t = core.useTranslation();
832
- const [rows, setRows] = React2__namespace.useState(null);
833
- React2__namespace.useEffect(() => {
834
- const p = courses.getCoursesProvider();
835
- void Promise.all([p.listCourses(), p.listOrders({ financialStatus: "paid" })]).then(([courses, orders]) => {
836
- setRows(buildRevenueByCourse(courses, orders));
837
- });
838
- }, []);
839
- const columns = [
840
- { key: "title", header: t("courses.reports.course") || "Course", render: (r) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: r.title }) },
841
- { key: "sales", header: t("courses.reports.sales") || "Sales", align: "right", render: (r) => /* @__PURE__ */ jsxRuntime.jsx("span", { children: r.sales }) },
842
- { key: "revenue", header: t("courses.reports.revenue") || "Revenue", align: "right", render: (r) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: formatMoney(r.revenue, r.currency) }) }
843
- ];
844
- return /* @__PURE__ */ jsxRuntime.jsxs(PageContainer, { title: t("courses.reports.title") || "Reports", subtitle: t("courses.reports.subtitle") || "Revenue by course.", children: [
845
- /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "mb-3 text-sm font-semibold text-foreground", children: t("courses.reports.byCourse") || "Revenue by course" }),
846
- /* @__PURE__ */ jsxRuntime.jsx(
847
- SimpleTable,
848
- {
849
- columns,
850
- rows: rows ?? [],
851
- empty: t("courses.reports.empty") || "No revenue yet."
852
- }
853
- )
854
- ] });
855
- }
856
- ReportsPage.displayName = "ReportsPage";
857
- function buildRevenueByCourse(courses, orders) {
858
- const byCourse = /* @__PURE__ */ new Map();
859
- for (const o of orders) {
860
- const agg = byCourse.get(o.courseId) ?? { sales: 0, revenue: 0, currency: o.currency };
861
- agg.sales += 1;
862
- agg.revenue = Math.round((agg.revenue + o.total) * 100) / 100;
863
- byCourse.set(o.courseId, agg);
864
- }
865
- return courses.map((c) => {
866
- const agg = byCourse.get(c.id);
867
- return { courseId: c.id, title: c.title, sales: agg?.sales ?? 0, revenue: agg?.revenue ?? 0, currency: agg?.currency ?? c.currency };
868
- }).sort((a, b) => b.revenue - a.revenue);
869
- }
870
- function MembersAreaPage() {
871
- const t = core.useTranslation();
872
- const [rows, setRows] = React2__namespace.useState(null);
873
- React2__namespace.useEffect(() => {
874
- const p = courses.getCoursesProvider();
875
- void Promise.all([p.listCourses(), p.listOrders({ financialStatus: "paid" })]).then(([courses, orders]) => {
876
- setRows(buildMembersAreas(courses, orders));
877
- });
878
- }, []);
879
- const columns = [
880
- { key: "title", header: t("courses.membersArea.name") || "Members area", render: (r) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: r.title }) },
881
- { key: "status", header: t("courses.membersArea.status") || "Status", render: (r) => /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { variant: r.status === "published" ? "success" : r.status === "archived" ? "outline" : "secondary", children: t(`courses.status.${r.status}`) || r.status }) },
882
- { key: "students", header: t("courses.membersArea.students") || "Students", align: "right", render: (r) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-foreground", children: r.students }) }
883
- ];
884
- return /* @__PURE__ */ jsxRuntime.jsx(PageContainer, { title: t("courses.membersArea.title") || "Members area", subtitle: t("courses.membersArea.subtitle") || "One area per course; students land here after purchase.", children: /* @__PURE__ */ jsxRuntime.jsx(
885
- SimpleTable,
886
- {
887
- columns,
888
- rows: rows ?? [],
889
- empty: t("courses.membersArea.empty") || "No members areas yet."
890
- }
891
- ) });
892
- }
893
- MembersAreaPage.displayName = "MembersAreaPage";
894
- function buildMembersAreas(courses, orders) {
895
- const students = /* @__PURE__ */ new Map();
896
- for (const o of orders) {
897
- const set = students.get(o.courseId) ?? /* @__PURE__ */ new Set();
898
- if (o.customerId) set.add(o.customerId);
899
- students.set(o.courseId, set);
900
- }
901
- return courses.map((c) => ({
902
- courseId: c.id,
903
- title: c.title,
904
- status: c.status,
905
- students: students.get(c.id)?.size ?? 0
906
- }));
907
- }
908
- function CoursesGeneralSettings() {
909
- const t = core.useTranslation();
910
- const [currency, setCurrency] = React2__namespace.useState("BRL");
911
- const [methods, setMethods] = React2__namespace.useState({ card: true, pix: true, boleto: true });
912
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-6", children: [
913
- /* @__PURE__ */ jsxRuntime.jsx(
914
- saas.SettingsGroup,
915
- {
916
- title: t("courses.settings.payments") || "Payments",
917
- description: t("courses.settings.paymentsHelp") || "Connect Stripe in the Integrations tab; the platform fee applies automatically.",
918
- children: /* @__PURE__ */ jsxRuntime.jsx(
919
- saas.SelectRow,
920
- {
921
- label: t("courses.settings.currency") || "Default currency",
922
- value: currency,
923
- onChange: setCurrency,
924
- options: [
925
- { value: "BRL", label: "BRL (R$)" },
926
- { value: "USD", label: "USD ($)" },
927
- { value: "EUR", label: "EUR (\u20AC)" }
928
- ]
929
- }
930
- )
931
- }
932
- ),
933
- /* @__PURE__ */ jsxRuntime.jsxs(saas.SettingsGroup, { title: t("courses.settings.methods") || "Payment methods", children: [
934
- /* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t("courses.editor.payCard") || "Credit card", checked: methods.card, onChange: (v) => setMethods((m) => ({ ...m, card: v })) }),
935
- /* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t("courses.editor.payPix") || "Pix", checked: methods.pix, onChange: (v) => setMethods((m) => ({ ...m, pix: v })) }),
936
- /* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t("courses.editor.payBoleto") || "Boleto", checked: methods.boleto, onChange: (v) => setMethods((m) => ({ ...m, boleto: v })) })
937
- ] })
938
- ] });
939
- }
940
- CoursesGeneralSettings.displayName = "CoursesGeneralSettings";
941
- function CoursesSettingsTab() {
942
- const t = core.useTranslation();
943
- return /* @__PURE__ */ jsxRuntime.jsx(
944
- saas.PluginSettingsPanel,
945
- {
946
- title: t("courses.settings.title") || "Course settings",
947
- subtitle: t("courses.settings.subtitle") || "Payments, currency and checkout methods",
948
- generalSettings: /* @__PURE__ */ jsxRuntime.jsx(CoursesGeneralSettings, {}),
949
- hostPluginId: "courses",
950
- routeBase: "/settings/courses"
951
- }
952
- );
953
- }
954
- CoursesSettingsTab.displayName = "CoursesSettingsTab";
955
- function useSummary() {
956
- const [summary, setSummary] = React2__namespace.useState(null);
957
- React2__namespace.useEffect(() => {
958
- void courses.getCoursesProvider().getFinancialSummary().then(setSummary);
959
- }, []);
960
- return summary;
961
- }
962
- function RevenueKpi() {
963
- const t = core.useTranslation();
964
- const s = useSummary();
965
- return /* @__PURE__ */ jsxRuntime.jsx(ui.KpiCard, { label: t("courses.kpi.revenue") || "Total revenue", icon: "DollarSign", loading: !s, value: s ? formatMoney(s.totalRevenue, s.currency) : void 0 });
966
- }
967
- function SalesKpi() {
968
- const t = core.useTranslation();
969
- const s = useSummary();
970
- return /* @__PURE__ */ jsxRuntime.jsx(ui.KpiCard, { label: t("courses.kpi.sales") || "Sales", icon: "TrendingUp", loading: !s, value: s ? String(s.salesCount) : void 0, sub: s ? formatPercent(s.refundRate, 1) + " " + (t("courses.kpi.refunds") || "refunds") : void 0 });
971
- }
972
- function MrrKpi() {
973
- const t = core.useTranslation();
974
- const s = useSummary();
975
- return /* @__PURE__ */ jsxRuntime.jsx(ui.KpiCard, { label: t("courses.kpi.mrr") || "MRR", icon: "Repeat", loading: !s, value: s ? formatMoney(s.mrr, s.currency) : void 0, sub: s ? `${s.activeSubscriptions} ${t("courses.kpi.activeSubs") || "active"}` : void 0 });
976
- }
977
- function FeesKpi() {
978
- const t = core.useTranslation();
979
- const s = useSummary();
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 });
981
- }
982
- function createCoursesDashboardWidgets(modules = {}) {
983
- return [
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 })] : []
990
- ];
991
- }
992
- function createStripeConnector() {
993
- return {
994
- id: "stripe-connect",
995
- hostPluginId: "courses",
996
- name: "Stripe",
997
- description: "Receba os pagamentos dos alunos; a taxa da plataforma \xE9 aplicada automaticamente.",
998
- icon: "CreditCard",
999
- authKind: "oauth",
1000
- async getStatus() {
1001
- try {
1002
- const account = await courses.getCoursesProvider().getCreatorAccount();
1003
- const fee = (account.platformFeeBps / 100).toFixed(2);
1004
- return {
1005
- connected: !!account.stripeAccountId && account.chargesEnabled,
1006
- detail: `Taxa da plataforma: ${fee}%`
1007
- };
1008
- } catch {
1009
- return { connected: false };
1010
- }
1011
- },
1012
- async startOAuth(_redirectTo) {
1013
- throw new Error("O onboarding do Stripe Connect \xE9 ligado na Etapa 3.");
1014
- }
1015
- };
1016
- }
1017
-
1018
- // src/index.ts
1019
- var asComp = (c) => c;
1020
- var read = (feature) => ({ feature, action: "read" });
1021
- function createCoursesPlugin(options) {
1022
- core.registerTranslations(coursesLocales);
1023
- courses.setCoursesTenantResolver(() => saas.useOrganizationStore.getState().currentOrg?.id);
1024
- if (!courses.getCoursesProviderOptional()) {
1025
- courses.setCoursesProvider(courses.createMockCoursesProvider());
1026
- }
1027
- const base = options?.navPosition ?? 1;
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
- ];
1050
- return {
1051
- id: "courses",
1052
- name: options?.navLabel ?? "Courses",
1053
- icon: "BookOpen",
1054
- version: "1.0.0",
1055
- scope: options?.scope ?? "vertical",
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"],
1061
- defaultEnabled: true,
1062
- dependencies: [],
1063
- declaredFeatures: [
1064
- { id: "courses", label: "Courses", group: "Courses" },
1065
- { id: "courses.sales", label: "Sales", group: "Courses" },
1066
- { id: "courses.finance", label: "Financial", group: "Courses" }
1067
- ],
1068
- declaredLimits: [
1069
- { key: "courses", label: "Courses", table: "plg_courses_courses" }
1070
- ],
1071
- navigation,
1072
- routes,
1073
- widgets: [],
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),
1077
- connectors: [createStripeConnector()],
1078
- settings: [
1079
- { id: "courses", label: "Courses", icon: "BookOpen", component: asComp(CoursesSettingsTab), order: 20, permission: read("courses") }
1080
- ],
1081
- locales: coursesLocales
1082
- };
1083
- }
1084
- core.registerPluginFactory("courses", createCoursesPlugin);
1085
-
1086
- exports.coursesLocales = coursesLocales;
1087
- exports.createCoursesPlugin = createCoursesPlugin;
1088
- //# sourceMappingURL=index.cjs.map
1089
- //# sourceMappingURL=index.cjs.map