@fayz-ai/plugin-courses 0.8.4 → 0.9.0-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +13 -13
- package/dist/index.cjs +0 -2025
- package/dist/index.cjs.map +0 -1
package/dist/index.cjs
DELETED
|
@@ -1,2025 +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 React3 = require('react');
|
|
7
|
-
var ui = require('@fayz-ai/ui');
|
|
8
|
-
var jsxRuntime = require('react/jsx-runtime');
|
|
9
|
-
var lucideReact = require('lucide-react');
|
|
10
|
-
var core$1 = require('@dnd-kit/core');
|
|
11
|
-
var sortable = require('@dnd-kit/sortable');
|
|
12
|
-
var utilities = require('@dnd-kit/utilities');
|
|
13
|
-
|
|
14
|
-
function _interopNamespace(e) {
|
|
15
|
-
if (e && e.__esModule) return e;
|
|
16
|
-
var n = Object.create(null);
|
|
17
|
-
if (e) {
|
|
18
|
-
Object.keys(e).forEach(function (k) {
|
|
19
|
-
if (k !== 'default') {
|
|
20
|
-
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
21
|
-
Object.defineProperty(n, k, d.get ? d : {
|
|
22
|
-
enumerable: true,
|
|
23
|
-
get: function () { return e[k]; }
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
n.default = e;
|
|
29
|
-
return Object.freeze(n);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
var React3__namespace = /*#__PURE__*/_interopNamespace(React3);
|
|
33
|
-
|
|
34
|
-
// src/index.ts
|
|
35
|
-
|
|
36
|
-
// src/locales/en.ts
|
|
37
|
-
var en = {
|
|
38
|
-
"courses.nav": "Courses",
|
|
39
|
-
"courses.title": "Courses",
|
|
40
|
-
"courses.new": "New course",
|
|
41
|
-
"courses.empty": "No courses yet. Create your first one.",
|
|
42
|
-
"courses.count": "{count} courses",
|
|
43
|
-
"courses.students": "students",
|
|
44
|
-
"courses.lessons": "lessons",
|
|
45
|
-
"courses.status.draft": "Draft",
|
|
46
|
-
"courses.status.published": "Published",
|
|
47
|
-
"courses.status.archived": "Archived",
|
|
48
|
-
"courses.editor.back": "Back to courses",
|
|
49
|
-
"courses.editor.details": "Details",
|
|
50
|
-
"courses.editor.curriculum": "Curriculum",
|
|
51
|
-
"courses.editor.fieldTitle": "Title",
|
|
52
|
-
"courses.editor.fieldSubtitle": "Subtitle",
|
|
53
|
-
"courses.editor.fieldDescription": "Description",
|
|
54
|
-
"courses.editor.fieldPrice": "Price",
|
|
55
|
-
"courses.editor.fieldStatus": "Status",
|
|
56
|
-
"courses.editor.save": "Save",
|
|
57
|
-
"courses.editor.saved": "Course saved",
|
|
58
|
-
"courses.editor.saveFailed": "Could not save the course",
|
|
59
|
-
"courses.editor.unsaved": "Unsaved changes",
|
|
60
|
-
// General tab
|
|
61
|
-
"courses.editor.sectionBasics": "Basics",
|
|
62
|
-
"courses.editor.sectionThumbnail": "Cover image",
|
|
63
|
-
"courses.editor.sectionPricing": "Pricing & status",
|
|
64
|
-
"courses.editor.descriptionPlaceholder": "What will students learn?",
|
|
65
|
-
"courses.editor.fieldDelivery": "Format",
|
|
66
|
-
"courses.editor.deliveryHint": "How this course is delivered.",
|
|
67
|
-
"courses.editor.delivery.online": "Online",
|
|
68
|
-
"courses.editor.delivery.in_person": "In person",
|
|
69
|
-
"courses.editor.delivery.hybrid": "Hybrid",
|
|
70
|
-
"courses.editor.thumbnailHint": "Shown on the storefront and in listings. 1280\xD7720 recommended.",
|
|
71
|
-
"courses.editor.thumbnailUpload": "Upload image",
|
|
72
|
-
"courses.editor.thumbnailUrl": "Or paste an image URL",
|
|
73
|
-
"courses.editor.thumbnailEmpty": "No image",
|
|
74
|
-
"courses.editor.thumbnailUploaded": "Image uploaded \u2014 remember to save.",
|
|
75
|
-
"courses.editor.uploading": "Uploading\u2026",
|
|
76
|
-
"courses.editor.uploadFailed": "Could not upload the image",
|
|
77
|
-
"courses.editor.uploadUnavailable": "Upload unavailable \u2014 paste an image URL instead.",
|
|
78
|
-
// Curriculum tab
|
|
79
|
-
"courses.editor.addModule": "Add module",
|
|
80
|
-
"courses.editor.moduleTitle": "Module title",
|
|
81
|
-
"courses.editor.moduleAdded": "Module added",
|
|
82
|
-
"courses.editor.addLesson": "Add lesson",
|
|
83
|
-
"courses.editor.lessonTitle": "Lesson title",
|
|
84
|
-
"courses.editor.lessonVideo": "Video URL (embed)",
|
|
85
|
-
"courses.editor.lessonDuration": "Duration (min)",
|
|
86
|
-
"courses.editor.deleteModule": "Delete module",
|
|
87
|
-
"courses.editor.deleteLesson": "Remove lesson",
|
|
88
|
-
"courses.editor.deleted": "Removed",
|
|
89
|
-
"courses.editor.autosaved": "Changes are saved automatically.",
|
|
90
|
-
"courses.editor.modulesEmpty": "No modules yet",
|
|
91
|
-
"courses.editor.modulesEmptyHint": "Create a module to start organizing your lessons.",
|
|
92
|
-
"courses.editor.lessonsEmpty": "No lessons in this module yet.",
|
|
93
|
-
"courses.editor.deleteModuleConfirm": "Delete this module?",
|
|
94
|
-
"courses.editor.deleteModuleConfirmBody": "Its lessons will be removed too. This cannot be undone.",
|
|
95
|
-
"courses.editor.deleteLessonConfirm": "Remove this lesson?",
|
|
96
|
-
"courses.editor.deleteLessonConfirmBody": "This cannot be undone.",
|
|
97
|
-
"courses.editor.confirmDelete": "Delete",
|
|
98
|
-
"courses.editor.cancel": "Cancel",
|
|
99
|
-
// Curriculum tree builder
|
|
100
|
-
"courses.editor.newModule": "New module",
|
|
101
|
-
"courses.editor.firstModule": "Create first module",
|
|
102
|
-
"courses.editor.newLesson": "New lesson",
|
|
103
|
-
"courses.editor.firstLesson": "Add the first lesson",
|
|
104
|
-
"courses.editor.moduleLabel": "module",
|
|
105
|
-
"courses.editor.moduleLabelPlural": "modules",
|
|
106
|
-
"courses.editor.lessonUnitOne": "lesson",
|
|
107
|
-
"courses.editor.lessonUnitOther": "lessons",
|
|
108
|
-
"courses.editor.noLessons": "No lessons",
|
|
109
|
-
"courses.editor.minShort": "min",
|
|
110
|
-
"courses.editor.hourShort": "h",
|
|
111
|
-
"courses.editor.dragToReorder": "Drag to reorder",
|
|
112
|
-
"courses.editor.expand": "Expand",
|
|
113
|
-
"courses.editor.collapse": "Collapse",
|
|
114
|
-
"courses.editor.editLesson": "Edit lesson",
|
|
115
|
-
"courses.editor.addVideo": "Add video",
|
|
116
|
-
"courses.editor.videoSet": "Video",
|
|
117
|
-
"courses.editor.videoUrlLabel": "Video URL",
|
|
118
|
-
"courses.editor.videoUrlPlaceholder": "Paste the video link (YouTube, Vimeo\u2026)",
|
|
119
|
-
"courses.editor.reorderFailed": "Could not reorder",
|
|
120
|
-
// Members area
|
|
121
|
-
"courses.editor.membersHelp": "Buyers are enrolled automatically through orders and get access to all of this course\u2019s content.",
|
|
122
|
-
"courses.editor.enrolledStudents": "Enrolled students",
|
|
123
|
-
"courses.editor.membersEmpty": "No students yet",
|
|
124
|
-
"courses.editor.membersEmptyHint": "Buyers are enrolled here automatically after a paid order.",
|
|
125
|
-
"courses.editor.accessUnlockedBy": "Access is unlocked by",
|
|
126
|
-
"courses.editor.membersNote": "Buying any offer grants full access to this course\u2019s content.",
|
|
127
|
-
"courses.editor.untitledCourse": "Untitled course",
|
|
128
|
-
"courses.editor.untitledModule": "New module",
|
|
129
|
-
"courses.editor.untitledLesson": "New lesson",
|
|
130
|
-
// Editor tabs
|
|
131
|
-
"courses.editor.tab.details": "General",
|
|
132
|
-
"courses.editor.tab.curriculum": "Curriculum",
|
|
133
|
-
"courses.editor.tab.offers": "Offers",
|
|
134
|
-
"courses.editor.tab.members": "Members area",
|
|
135
|
-
// Payment methods (used by the Courses plugin settings surface)
|
|
136
|
-
"courses.editor.payCard": "Credit card (up to 12x)",
|
|
137
|
-
"courses.editor.payPix": "Pix",
|
|
138
|
-
"courses.editor.payBoleto": "Boleto",
|
|
139
|
-
// Offers
|
|
140
|
-
"courses.offers.help": "Price points for this course. Sales run through orders and the Financeiro module.",
|
|
141
|
-
"courses.offers.name": "Offer name",
|
|
142
|
-
"courses.offers.price": "Price",
|
|
143
|
-
"courses.offers.kind": "Type",
|
|
144
|
-
"courses.offers.oneTime": "One-time",
|
|
145
|
-
"courses.offers.subscription": "Subscription",
|
|
146
|
-
"courses.offers.interval": "Billing interval",
|
|
147
|
-
"courses.offers.default": "Default",
|
|
148
|
-
"courses.offers.bump": "Order bump",
|
|
149
|
-
"courses.offers.remove": "Remove",
|
|
150
|
-
"courses.offers.add": "Add offer",
|
|
151
|
-
"courses.offers.added": "Offer added",
|
|
152
|
-
"courses.offers.removed": "Offer removed",
|
|
153
|
-
"courses.offers.newName": "New offer",
|
|
154
|
-
"courses.offers.empty": "No offers yet",
|
|
155
|
-
"courses.offers.emptyHint": "Create an offer to sell this course.",
|
|
156
|
-
"courses.offers.firstOffer": "Create first offer",
|
|
157
|
-
"courses.offers.checkoutFlagsNote": "Apply once the public checkout ships (coming soon).",
|
|
158
|
-
// Sales
|
|
159
|
-
"courses.sales.title": "Sales",
|
|
160
|
-
"courses.sales.subtitle": "Every transaction across your courses.",
|
|
161
|
-
"courses.sales.date": "Date",
|
|
162
|
-
"courses.sales.customer": "Customer",
|
|
163
|
-
"courses.sales.method": "Method",
|
|
164
|
-
"courses.sales.status": "Status",
|
|
165
|
-
"courses.sales.fee": "Platform fee",
|
|
166
|
-
"courses.sales.amount": "Amount",
|
|
167
|
-
"courses.sales.count": "Sales",
|
|
168
|
-
"courses.sales.revenue": "Revenue",
|
|
169
|
-
"courses.sales.empty": "No sales yet.",
|
|
170
|
-
"courses.financialStatus.paid": "Paid",
|
|
171
|
-
"courses.financialStatus.pending": "Pending",
|
|
172
|
-
"courses.financialStatus.refunded": "Refunded",
|
|
173
|
-
"courses.financialStatus.chargeback": "Chargeback",
|
|
174
|
-
// Subscriptions
|
|
175
|
-
"courses.subs.title": "Subscriptions",
|
|
176
|
-
"courses.subs.subtitle": "Recurring plans and MRR.",
|
|
177
|
-
"courses.subs.start": "Start date",
|
|
178
|
-
"courses.subs.customer": "Customer",
|
|
179
|
-
"courses.subs.status": "Status",
|
|
180
|
-
"courses.subs.value": "Net value",
|
|
181
|
-
"courses.subs.active": "Active subscriptions",
|
|
182
|
-
"courses.subs.mrr": "Monthly recurring revenue",
|
|
183
|
-
"courses.subs.empty": "No subscriptions yet.",
|
|
184
|
-
"courses.subStatus.active": "Active",
|
|
185
|
-
"courses.subStatus.canceled": "Canceled",
|
|
186
|
-
"courses.subStatus.past_due": "Past due",
|
|
187
|
-
"courses.interval.month": "month",
|
|
188
|
-
"courses.interval.year": "year",
|
|
189
|
-
// Financial
|
|
190
|
-
"courses.financial.title": "Financial settings",
|
|
191
|
-
"courses.financial.subtitle": "Balance, payouts and your Stripe connection.",
|
|
192
|
-
"courses.financial.available": "Available balance",
|
|
193
|
-
"courses.financial.pending": "Pending balance",
|
|
194
|
-
"courses.financial.revenue": "Total revenue",
|
|
195
|
-
"courses.financial.feeCollected": "Platform fees",
|
|
196
|
-
"courses.financial.refundRate": "refund rate",
|
|
197
|
-
"courses.financial.payouts": "Payouts",
|
|
198
|
-
"courses.financial.noPayouts": "No payouts yet.",
|
|
199
|
-
"courses.financial.date": "Date",
|
|
200
|
-
"courses.financial.amount": "Amount",
|
|
201
|
-
"courses.financial.status": "Status",
|
|
202
|
-
"courses.financial.connect": "Connect Stripe",
|
|
203
|
-
"courses.financial.connected": "Stripe connected",
|
|
204
|
-
"courses.financial.notConnected": "Connect your Stripe account to receive payouts. The platform fee is applied automatically on each sale.",
|
|
205
|
-
"courses.financial.platformFee": "Platform fee",
|
|
206
|
-
"courses.financial.connectSoon": "Stripe Connect onboarding wires up in Stage 3.",
|
|
207
|
-
"courses.payoutStatus.paid": "Paid",
|
|
208
|
-
"courses.payoutStatus.pending": "Pending",
|
|
209
|
-
"courses.payoutStatus.failed": "Failed",
|
|
210
|
-
// Reports
|
|
211
|
-
"courses.reports.title": "Reports",
|
|
212
|
-
"courses.reports.subtitle": "Revenue by course.",
|
|
213
|
-
"courses.reports.byCourse": "Revenue by course",
|
|
214
|
-
"courses.reports.course": "Course",
|
|
215
|
-
"courses.reports.sales": "Sales",
|
|
216
|
-
"courses.reports.revenue": "Revenue",
|
|
217
|
-
"courses.reports.empty": "No revenue yet.",
|
|
218
|
-
// Members area
|
|
219
|
-
"courses.membersArea.title": "Members area",
|
|
220
|
-
"courses.membersArea.subtitle": "One area per course; students land here after purchase.",
|
|
221
|
-
"courses.membersArea.name": "Members area",
|
|
222
|
-
"courses.membersArea.status": "Status",
|
|
223
|
-
"courses.membersArea.students": "Students",
|
|
224
|
-
"courses.membersArea.empty": "No members areas yet.",
|
|
225
|
-
// Settings
|
|
226
|
-
"courses.settings.title": "Course settings",
|
|
227
|
-
"courses.settings.subtitle": "Payments, currency and checkout methods",
|
|
228
|
-
"courses.settings.payments": "Payments",
|
|
229
|
-
"courses.settings.paymentsHelp": "Connect Stripe in the Integrations tab; the platform fee applies automatically.",
|
|
230
|
-
"courses.settings.currency": "Default currency",
|
|
231
|
-
"courses.settings.methods": "Payment methods",
|
|
232
|
-
// Dashboard KPIs
|
|
233
|
-
"courses.kpi.revenue": "Total revenue",
|
|
234
|
-
"courses.kpi.sales": "Sales",
|
|
235
|
-
"courses.kpi.refunds": "refunds",
|
|
236
|
-
"courses.kpi.mrr": "Recurring revenue (MRR)",
|
|
237
|
-
"courses.kpi.activeSubs": "active",
|
|
238
|
-
"courses.kpi.fees": "Platform fees"
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
// src/locales/pt-BR.ts
|
|
242
|
-
var ptBR = {
|
|
243
|
-
"courses.nav": "Cursos",
|
|
244
|
-
"courses.title": "Cursos",
|
|
245
|
-
"courses.new": "Novo curso",
|
|
246
|
-
"courses.empty": "Nenhum curso ainda. Crie o primeiro.",
|
|
247
|
-
"courses.count": "{count} cursos",
|
|
248
|
-
"courses.students": "alunos",
|
|
249
|
-
"courses.lessons": "aulas",
|
|
250
|
-
"courses.status.draft": "Rascunho",
|
|
251
|
-
"courses.status.published": "Publicado",
|
|
252
|
-
"courses.status.archived": "Arquivado",
|
|
253
|
-
"courses.editor.back": "Voltar aos cursos",
|
|
254
|
-
"courses.editor.details": "Detalhes",
|
|
255
|
-
"courses.editor.curriculum": "Conte\xFAdo",
|
|
256
|
-
"courses.editor.fieldTitle": "T\xEDtulo",
|
|
257
|
-
"courses.editor.fieldSubtitle": "Subt\xEDtulo",
|
|
258
|
-
"courses.editor.fieldDescription": "Descri\xE7\xE3o",
|
|
259
|
-
"courses.editor.fieldPrice": "Pre\xE7o",
|
|
260
|
-
"courses.editor.fieldStatus": "Status",
|
|
261
|
-
"courses.editor.save": "Salvar",
|
|
262
|
-
"courses.editor.saved": "Curso salvo",
|
|
263
|
-
"courses.editor.saveFailed": "N\xE3o foi poss\xEDvel salvar o curso",
|
|
264
|
-
"courses.editor.unsaved": "Altera\xE7\xF5es n\xE3o salvas",
|
|
265
|
-
// Geral tab
|
|
266
|
-
"courses.editor.sectionBasics": "Informa\xE7\xF5es b\xE1sicas",
|
|
267
|
-
"courses.editor.sectionThumbnail": "Imagem de capa",
|
|
268
|
-
"courses.editor.sectionPricing": "Pre\xE7o e publica\xE7\xE3o",
|
|
269
|
-
"courses.editor.descriptionPlaceholder": "O que o aluno vai aprender?",
|
|
270
|
-
"courses.editor.fieldDelivery": "Formato",
|
|
271
|
-
"courses.editor.deliveryHint": "Como o curso \xE9 entregue.",
|
|
272
|
-
"courses.editor.delivery.online": "Online",
|
|
273
|
-
"courses.editor.delivery.in_person": "Presencial",
|
|
274
|
-
"courses.editor.delivery.hybrid": "H\xEDbrido",
|
|
275
|
-
"courses.editor.thumbnailHint": "Aparece na loja e nas listagens. Recomendado 1280\xD7720.",
|
|
276
|
-
"courses.editor.thumbnailUpload": "Enviar imagem",
|
|
277
|
-
"courses.editor.thumbnailUrl": "Ou cole uma URL de imagem",
|
|
278
|
-
"courses.editor.thumbnailEmpty": "Sem imagem",
|
|
279
|
-
"courses.editor.thumbnailUploaded": "Imagem enviada \u2014 lembre de salvar.",
|
|
280
|
-
"courses.editor.uploading": "Enviando\u2026",
|
|
281
|
-
"courses.editor.uploadFailed": "N\xE3o foi poss\xEDvel enviar a imagem",
|
|
282
|
-
"courses.editor.uploadUnavailable": "Upload indispon\xEDvel \u2014 cole uma URL de imagem.",
|
|
283
|
-
// Conteúdo tab
|
|
284
|
-
"courses.editor.addModule": "Adicionar m\xF3dulo",
|
|
285
|
-
"courses.editor.moduleTitle": "T\xEDtulo do m\xF3dulo",
|
|
286
|
-
"courses.editor.moduleAdded": "M\xF3dulo adicionado",
|
|
287
|
-
"courses.editor.addLesson": "Adicionar aula",
|
|
288
|
-
"courses.editor.lessonTitle": "T\xEDtulo da aula",
|
|
289
|
-
"courses.editor.lessonVideo": "URL do v\xEDdeo (embed)",
|
|
290
|
-
"courses.editor.lessonDuration": "Dura\xE7\xE3o (min)",
|
|
291
|
-
"courses.editor.deleteModule": "Excluir m\xF3dulo",
|
|
292
|
-
"courses.editor.deleteLesson": "Remover aula",
|
|
293
|
-
"courses.editor.deleted": "Removido",
|
|
294
|
-
"courses.editor.autosaved": "As altera\xE7\xF5es s\xE3o salvas automaticamente.",
|
|
295
|
-
"courses.editor.modulesEmpty": "Nenhum m\xF3dulo ainda",
|
|
296
|
-
"courses.editor.modulesEmptyHint": "Crie um m\xF3dulo para come\xE7ar a organizar as aulas.",
|
|
297
|
-
"courses.editor.lessonsEmpty": "Nenhuma aula neste m\xF3dulo ainda.",
|
|
298
|
-
"courses.editor.deleteModuleConfirm": "Excluir este m\xF3dulo?",
|
|
299
|
-
"courses.editor.deleteModuleConfirmBody": "As aulas deste m\xF3dulo tamb\xE9m ser\xE3o removidas. Isso n\xE3o pode ser desfeito.",
|
|
300
|
-
"courses.editor.deleteLessonConfirm": "Remover esta aula?",
|
|
301
|
-
"courses.editor.deleteLessonConfirmBody": "Isso n\xE3o pode ser desfeito.",
|
|
302
|
-
"courses.editor.confirmDelete": "Excluir",
|
|
303
|
-
"courses.editor.cancel": "Cancelar",
|
|
304
|
-
// Curriculum tree builder
|
|
305
|
-
"courses.editor.newModule": "Novo m\xF3dulo",
|
|
306
|
-
"courses.editor.firstModule": "Criar primeiro m\xF3dulo",
|
|
307
|
-
"courses.editor.newLesson": "Nova aula",
|
|
308
|
-
"courses.editor.firstLesson": "Adicione a primeira aula",
|
|
309
|
-
"courses.editor.moduleLabel": "m\xF3dulo",
|
|
310
|
-
"courses.editor.moduleLabelPlural": "m\xF3dulos",
|
|
311
|
-
"courses.editor.lessonUnitOne": "aula",
|
|
312
|
-
"courses.editor.lessonUnitOther": "aulas",
|
|
313
|
-
"courses.editor.noLessons": "Sem aulas",
|
|
314
|
-
"courses.editor.minShort": "min",
|
|
315
|
-
"courses.editor.hourShort": "h",
|
|
316
|
-
"courses.editor.dragToReorder": "Arraste para reordenar",
|
|
317
|
-
"courses.editor.expand": "Expandir",
|
|
318
|
-
"courses.editor.collapse": "Recolher",
|
|
319
|
-
"courses.editor.editLesson": "Editar aula",
|
|
320
|
-
"courses.editor.addVideo": "Adicionar v\xEDdeo",
|
|
321
|
-
"courses.editor.videoSet": "V\xEDdeo",
|
|
322
|
-
"courses.editor.videoUrlLabel": "URL do v\xEDdeo",
|
|
323
|
-
"courses.editor.videoUrlPlaceholder": "Cole o link do v\xEDdeo (YouTube, Vimeo\u2026)",
|
|
324
|
-
"courses.editor.reorderFailed": "N\xE3o foi poss\xEDvel reordenar",
|
|
325
|
-
// Área de membros
|
|
326
|
-
"courses.editor.membersHelp": "Quem compra este curso \xE9 matriculado automaticamente pelos pedidos e ganha acesso a todo o conte\xFAdo.",
|
|
327
|
-
"courses.editor.enrolledStudents": "Alunos matriculados",
|
|
328
|
-
"courses.editor.membersEmpty": "Nenhum aluno ainda",
|
|
329
|
-
"courses.editor.membersEmptyHint": "Os compradores s\xE3o matriculados aqui automaticamente ap\xF3s um pedido pago.",
|
|
330
|
-
"courses.editor.accessUnlockedBy": "Acesso liberado por",
|
|
331
|
-
"courses.editor.membersNote": "Comprar qualquer oferta libera o acesso completo ao conte\xFAdo deste curso.",
|
|
332
|
-
"courses.editor.untitledCourse": "Curso sem t\xEDtulo",
|
|
333
|
-
"courses.editor.untitledModule": "Novo m\xF3dulo",
|
|
334
|
-
"courses.editor.untitledLesson": "Nova aula",
|
|
335
|
-
// Editor tabs
|
|
336
|
-
"courses.editor.tab.details": "Geral",
|
|
337
|
-
"courses.editor.tab.curriculum": "Conte\xFAdo",
|
|
338
|
-
"courses.editor.tab.offers": "Ofertas",
|
|
339
|
-
"courses.editor.tab.members": "\xC1rea de membros",
|
|
340
|
-
// Métodos de pagamento (usados na tela de configurações do plugin de Cursos)
|
|
341
|
-
"courses.editor.payCard": "Cart\xE3o de cr\xE9dito (at\xE9 12x)",
|
|
342
|
-
"courses.editor.payPix": "Pix",
|
|
343
|
-
"courses.editor.payBoleto": "Boleto",
|
|
344
|
-
// Offers
|
|
345
|
-
"courses.offers.help": "Pre\xE7os deste curso. As vendas acontecem pelos pedidos e pelo Financeiro.",
|
|
346
|
-
"courses.offers.name": "Nome da oferta",
|
|
347
|
-
"courses.offers.price": "Pre\xE7o",
|
|
348
|
-
"courses.offers.kind": "Tipo",
|
|
349
|
-
"courses.offers.oneTime": "\xDAnico",
|
|
350
|
-
"courses.offers.subscription": "Assinatura",
|
|
351
|
-
"courses.offers.interval": "Intervalo de cobran\xE7a",
|
|
352
|
-
"courses.offers.default": "Padr\xE3o",
|
|
353
|
-
"courses.offers.bump": "Order bump",
|
|
354
|
-
"courses.offers.remove": "Remover",
|
|
355
|
-
"courses.offers.add": "Adicionar oferta",
|
|
356
|
-
"courses.offers.added": "Oferta adicionada",
|
|
357
|
-
"courses.offers.removed": "Oferta removida",
|
|
358
|
-
"courses.offers.newName": "Nova oferta",
|
|
359
|
-
"courses.offers.empty": "Nenhuma oferta ainda",
|
|
360
|
-
"courses.offers.emptyHint": "Crie uma oferta para vender este curso.",
|
|
361
|
-
"courses.offers.firstOffer": "Criar primeira oferta",
|
|
362
|
-
"courses.offers.checkoutFlagsNote": "Aplicam-se quando o checkout p\xFAblico entrar no ar (em breve).",
|
|
363
|
-
// Sales
|
|
364
|
-
"courses.sales.title": "Vendas",
|
|
365
|
-
"courses.sales.subtitle": "Todas as transa\xE7\xF5es dos seus cursos.",
|
|
366
|
-
"courses.sales.date": "Data",
|
|
367
|
-
"courses.sales.customer": "Cliente",
|
|
368
|
-
"courses.sales.method": "M\xE9todo",
|
|
369
|
-
"courses.sales.status": "Status",
|
|
370
|
-
"courses.sales.fee": "Taxa da plataforma",
|
|
371
|
-
"courses.sales.amount": "Valor",
|
|
372
|
-
"courses.sales.count": "Vendas",
|
|
373
|
-
"courses.sales.revenue": "Receita",
|
|
374
|
-
"courses.sales.empty": "Nenhuma venda ainda.",
|
|
375
|
-
"courses.financialStatus.paid": "Pago",
|
|
376
|
-
"courses.financialStatus.pending": "Pendente",
|
|
377
|
-
"courses.financialStatus.refunded": "Reembolsado",
|
|
378
|
-
"courses.financialStatus.chargeback": "Chargeback",
|
|
379
|
-
// Subscriptions
|
|
380
|
-
"courses.subs.title": "Assinaturas",
|
|
381
|
-
"courses.subs.subtitle": "Planos recorrentes e MRR.",
|
|
382
|
-
"courses.subs.start": "In\xEDcio",
|
|
383
|
-
"courses.subs.customer": "Cliente",
|
|
384
|
-
"courses.subs.status": "Status",
|
|
385
|
-
"courses.subs.value": "Valor l\xEDquido",
|
|
386
|
-
"courses.subs.active": "Assinaturas ativas",
|
|
387
|
-
"courses.subs.mrr": "Receita recorrente mensal",
|
|
388
|
-
"courses.subs.empty": "Nenhuma assinatura ainda.",
|
|
389
|
-
"courses.subStatus.active": "Ativa",
|
|
390
|
-
"courses.subStatus.canceled": "Cancelada",
|
|
391
|
-
"courses.subStatus.past_due": "Em atraso",
|
|
392
|
-
"courses.interval.month": "m\xEAs",
|
|
393
|
-
"courses.interval.year": "ano",
|
|
394
|
-
// Financial
|
|
395
|
-
"courses.financial.title": "Financeiro",
|
|
396
|
-
"courses.financial.subtitle": "Saldo, saques e sua conex\xE3o com o Stripe.",
|
|
397
|
-
"courses.financial.available": "Saldo dispon\xEDvel",
|
|
398
|
-
"courses.financial.pending": "Saldo pendente",
|
|
399
|
-
"courses.financial.revenue": "Receita total",
|
|
400
|
-
"courses.financial.feeCollected": "Taxas da plataforma",
|
|
401
|
-
"courses.financial.refundRate": "taxa de reembolso",
|
|
402
|
-
"courses.financial.payouts": "Saques",
|
|
403
|
-
"courses.financial.noPayouts": "Nenhum saque ainda.",
|
|
404
|
-
"courses.financial.date": "Data",
|
|
405
|
-
"courses.financial.amount": "Valor",
|
|
406
|
-
"courses.financial.status": "Status",
|
|
407
|
-
"courses.financial.connect": "Conectar Stripe",
|
|
408
|
-
"courses.financial.connected": "Stripe conectado",
|
|
409
|
-
"courses.financial.notConnected": "Conecte sua conta Stripe para receber os saques. A taxa da plataforma \xE9 aplicada automaticamente em cada venda.",
|
|
410
|
-
"courses.financial.platformFee": "Taxa da plataforma",
|
|
411
|
-
"courses.financial.connectSoon": "O onboarding do Stripe Connect \xE9 ligado na Etapa 3.",
|
|
412
|
-
"courses.payoutStatus.paid": "Conclu\xEDdo",
|
|
413
|
-
"courses.payoutStatus.pending": "Pendente",
|
|
414
|
-
"courses.payoutStatus.failed": "Falhou",
|
|
415
|
-
// Reports
|
|
416
|
-
"courses.reports.title": "Relat\xF3rios",
|
|
417
|
-
"courses.reports.subtitle": "Receita por curso.",
|
|
418
|
-
"courses.reports.byCourse": "Receita por curso",
|
|
419
|
-
"courses.reports.course": "Curso",
|
|
420
|
-
"courses.reports.sales": "Vendas",
|
|
421
|
-
"courses.reports.revenue": "Receita",
|
|
422
|
-
"courses.reports.empty": "Nenhuma receita ainda.",
|
|
423
|
-
// Members area
|
|
424
|
-
"courses.membersArea.title": "\xC1rea de membros",
|
|
425
|
-
"courses.membersArea.subtitle": "Uma \xE1rea por curso; o aluno entra ap\xF3s a compra.",
|
|
426
|
-
"courses.membersArea.name": "\xC1rea de membros",
|
|
427
|
-
"courses.membersArea.status": "Status",
|
|
428
|
-
"courses.membersArea.students": "Alunos",
|
|
429
|
-
"courses.membersArea.empty": "Nenhuma \xE1rea de membros ainda.",
|
|
430
|
-
// Settings
|
|
431
|
-
"courses.settings.title": "Configura\xE7\xF5es do curso",
|
|
432
|
-
"courses.settings.subtitle": "Pagamentos, moeda e m\xE9todos de checkout",
|
|
433
|
-
"courses.settings.payments": "Pagamentos",
|
|
434
|
-
"courses.settings.paymentsHelp": "Conecte o Stripe na aba Integra\xE7\xF5es; a taxa da plataforma \xE9 aplicada automaticamente.",
|
|
435
|
-
"courses.settings.currency": "Moeda padr\xE3o",
|
|
436
|
-
"courses.settings.methods": "M\xE9todos de pagamento",
|
|
437
|
-
// Dashboard KPIs
|
|
438
|
-
"courses.kpi.revenue": "Receita total",
|
|
439
|
-
"courses.kpi.sales": "Vendas",
|
|
440
|
-
"courses.kpi.refunds": "reembolsos",
|
|
441
|
-
"courses.kpi.mrr": "Receita recorrente (MRR)",
|
|
442
|
-
"courses.kpi.activeSubs": "ativas",
|
|
443
|
-
"courses.kpi.fees": "Taxas da plataforma"
|
|
444
|
-
};
|
|
445
|
-
|
|
446
|
-
// src/locales/index.ts
|
|
447
|
-
var coursesLocales = {
|
|
448
|
-
en,
|
|
449
|
-
"pt-BR": ptBR
|
|
450
|
-
};
|
|
451
|
-
var adapter = core.hashRouterAdapter();
|
|
452
|
-
function navigateTo(to) {
|
|
453
|
-
adapter.navigate(to);
|
|
454
|
-
}
|
|
455
|
-
function statusVariant(status) {
|
|
456
|
-
if (status === "published") return "success";
|
|
457
|
-
if (status === "archived") return "outline";
|
|
458
|
-
return "secondary";
|
|
459
|
-
}
|
|
460
|
-
function CoursesListPage() {
|
|
461
|
-
const t = core.useTranslation();
|
|
462
|
-
const shellOwnsHeader = ui.useModuleLayout() === "tabs";
|
|
463
|
-
const [courses$1, setCourses] = React3__namespace.useState(null);
|
|
464
|
-
const [creating, setCreating] = React3__namespace.useState(false);
|
|
465
|
-
const guardCourses = saas.useLimitGuard("courses");
|
|
466
|
-
const can = saas.usePermissionOptional();
|
|
467
|
-
const load = React3__namespace.useCallback(async () => {
|
|
468
|
-
setCourses(await courses.getCoursesProvider().listCourses());
|
|
469
|
-
}, []);
|
|
470
|
-
React3__namespace.useEffect(() => {
|
|
471
|
-
void load();
|
|
472
|
-
}, [load]);
|
|
473
|
-
async function onCreate() {
|
|
474
|
-
if (!can("courses", "create")) return;
|
|
475
|
-
if (await guardCourses() === "blocked") return;
|
|
476
|
-
setCreating(true);
|
|
477
|
-
try {
|
|
478
|
-
const course = await courses.getCoursesProvider().createCourse({
|
|
479
|
-
title: t("courses.editor.untitledCourse") || "Untitled course",
|
|
480
|
-
status: "draft"
|
|
481
|
-
});
|
|
482
|
-
saas.invalidateLimit("courses");
|
|
483
|
-
navigateTo(`/courses/${course.id}`);
|
|
484
|
-
} finally {
|
|
485
|
-
setCreating(false);
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto max-w-6xl p-6 md:p-8", children: [
|
|
489
|
-
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: [
|
|
490
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
491
|
-
/* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-2xl font-bold text-foreground", children: t("courses.title") || "Courses" }),
|
|
492
|
-
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)) })
|
|
493
|
-
] }),
|
|
494
|
-
/* @__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" }) })
|
|
495
|
-
] }),
|
|
496
|
-
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." }),
|
|
497
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3", children: [
|
|
498
|
-
courses$1 === null && Array.from({ length: 6 }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(CourseCardSkeleton, {}, i)),
|
|
499
|
-
courses$1?.map((course) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
500
|
-
"button",
|
|
501
|
-
{
|
|
502
|
-
onClick: () => navigateTo(`/courses/${course.id}`),
|
|
503
|
-
className: "group overflow-hidden rounded-xl border border-border bg-card text-left transition hover:border-primary hover:shadow-md",
|
|
504
|
-
children: [
|
|
505
|
-
/* @__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
|
|
506
|
-
/* @__PURE__ */ jsxRuntime.jsx("img", { src: course.thumbnailUrl, alt: "", className: "h-full w-full object-cover" }) }),
|
|
507
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2 p-4", children: [
|
|
508
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start justify-between gap-2", children: [
|
|
509
|
-
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "font-semibold text-foreground line-clamp-2", children: course.title }),
|
|
510
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { variant: statusVariant(course.status), children: t(`courses.status.${course.status}`) || course.status })
|
|
511
|
-
] }),
|
|
512
|
-
course.subtitle && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground line-clamp-2", children: course.subtitle }),
|
|
513
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium text-foreground", children: course.price > 0 ? `${course.currency} ${course.price.toFixed(2)}` : "\u2014" })
|
|
514
|
-
] })
|
|
515
|
-
]
|
|
516
|
-
},
|
|
517
|
-
course.id
|
|
518
|
-
))
|
|
519
|
-
] })
|
|
520
|
-
] });
|
|
521
|
-
}
|
|
522
|
-
CoursesListPage.displayName = "CoursesListPage";
|
|
523
|
-
function CourseCardSkeleton() {
|
|
524
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "overflow-hidden rounded-xl border border-border bg-card", children: [
|
|
525
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "aspect-video w-full rounded-none" }),
|
|
526
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3 p-4", children: [
|
|
527
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start justify-between gap-2", children: [
|
|
528
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-4 w-2/3" }),
|
|
529
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-5 w-16 rounded-full" })
|
|
530
|
-
] }),
|
|
531
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-3 w-full" }),
|
|
532
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-4 w-20" })
|
|
533
|
-
] })
|
|
534
|
-
] });
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
// src/lib/format.ts
|
|
538
|
-
function formatMoney(value, currency = "BRL") {
|
|
539
|
-
try {
|
|
540
|
-
return new Intl.NumberFormat("pt-BR", { style: "currency", currency }).format(value);
|
|
541
|
-
} catch {
|
|
542
|
-
return `${currency} ${value.toFixed(2)}`;
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
function formatDate(iso) {
|
|
546
|
-
try {
|
|
547
|
-
return new Intl.DateTimeFormat("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric" }).format(new Date(iso));
|
|
548
|
-
} catch {
|
|
549
|
-
return iso.slice(0, 10);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
function formatPercent(fraction, digits = 0) {
|
|
553
|
-
return `${(fraction * 100).toFixed(digits)}%`;
|
|
554
|
-
}
|
|
555
|
-
function PageContainer({ title, subtitle, actions, children }) {
|
|
556
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto max-w-6xl p-6 md:p-8", children: [
|
|
557
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-6 flex items-start justify-between gap-4", children: [
|
|
558
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
559
|
-
/* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-2xl font-bold text-foreground", children: title }),
|
|
560
|
-
subtitle && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: subtitle })
|
|
561
|
-
] }),
|
|
562
|
-
actions
|
|
563
|
-
] }),
|
|
564
|
-
children
|
|
565
|
-
] });
|
|
566
|
-
}
|
|
567
|
-
function StatCard({ label, value, hint }) {
|
|
568
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Card, { className: "p-5", children: [
|
|
569
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: label }),
|
|
570
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-2xl font-bold text-foreground", children: value }),
|
|
571
|
-
hint && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: hint })
|
|
572
|
-
] });
|
|
573
|
-
}
|
|
574
|
-
function StatGrid({ children }) {
|
|
575
|
-
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4", children });
|
|
576
|
-
}
|
|
577
|
-
function EmptyState({ icon, title, description, action }) {
|
|
578
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("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", children: [
|
|
579
|
-
icon && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-3 flex h-11 w-11 items-center justify-center rounded-full bg-muted text-muted-foreground", children: icon }),
|
|
580
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium text-foreground", children: title }),
|
|
581
|
-
description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 max-w-sm text-sm text-muted-foreground", children: description }),
|
|
582
|
-
action && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-4", children: action })
|
|
583
|
-
] });
|
|
584
|
-
}
|
|
585
|
-
function InfoNote({ icon, children, className }) {
|
|
586
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("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 ?? ""), children: [
|
|
587
|
-
icon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mt-0.5 shrink-0 text-muted-foreground/80", children: icon }),
|
|
588
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-w-0 [&_strong]:font-medium [&_strong]:text-foreground", children })
|
|
589
|
-
] });
|
|
590
|
-
}
|
|
591
|
-
function SectionCard({ title, description, actions, children, className }) {
|
|
592
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Card, { className: "p-5 md:p-6 " + (className ?? ""), children: [
|
|
593
|
-
(title || actions) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-4 flex items-start justify-between gap-4", children: [
|
|
594
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
595
|
-
title && /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-sm font-semibold text-foreground", children: title }),
|
|
596
|
-
description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-0.5 text-sm text-muted-foreground", children: description })
|
|
597
|
-
] }),
|
|
598
|
-
actions
|
|
599
|
-
] }),
|
|
600
|
-
children
|
|
601
|
-
] });
|
|
602
|
-
}
|
|
603
|
-
function SimpleTable({ columns, rows, empty, loading, onRowClick }) {
|
|
604
|
-
const cols = React3__namespace.useMemo(
|
|
605
|
-
() => columns.map((c) => ({
|
|
606
|
-
id: c.key,
|
|
607
|
-
header: () => /* @__PURE__ */ jsxRuntime.jsx("div", { className: c.align === "right" ? "text-right" : "", children: c.header }),
|
|
608
|
-
cell: ({ row }) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: c.align === "right" ? "text-right" : "", children: c.render(row.original) })
|
|
609
|
-
})),
|
|
610
|
-
[columns]
|
|
611
|
-
);
|
|
612
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
613
|
-
ui.DataTable,
|
|
614
|
-
{
|
|
615
|
-
columns: cols,
|
|
616
|
-
data: rows,
|
|
617
|
-
emptyMessage: empty,
|
|
618
|
-
loading,
|
|
619
|
-
onRowClick: onRowClick ? (row) => onRowClick(row) : void 0
|
|
620
|
-
}
|
|
621
|
-
);
|
|
622
|
-
}
|
|
623
|
-
function buildCourseStudents(orders) {
|
|
624
|
-
const byKey = /* @__PURE__ */ new Map();
|
|
625
|
-
for (const o of orders) {
|
|
626
|
-
const key = o.customerId ?? o.customerEmail ?? o.id;
|
|
627
|
-
const existing = byKey.get(key);
|
|
628
|
-
if (!existing || o.createdAt && existing.since && o.createdAt > existing.since) {
|
|
629
|
-
byKey.set(key, {
|
|
630
|
-
key,
|
|
631
|
-
name: o.customerName ?? o.customerEmail ?? "\u2014",
|
|
632
|
-
email: o.customerEmail ?? null,
|
|
633
|
-
since: o.createdAt ?? null
|
|
634
|
-
});
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
return [...byKey.values()].sort((a, b) => (b.since ?? "").localeCompare(a.since ?? ""));
|
|
638
|
-
}
|
|
639
|
-
var STATUSES = ["draft", "published", "archived"];
|
|
640
|
-
var DELIVERY_TYPES = ["online", "in_person", "hybrid"];
|
|
641
|
-
var TABS = ["details", "curriculum", "offers", "members"];
|
|
642
|
-
var TAB_ICON = {
|
|
643
|
-
details: lucideReact.Tag,
|
|
644
|
-
curriculum: lucideReact.Layers,
|
|
645
|
-
offers: lucideReact.ShoppingCart,
|
|
646
|
-
members: lucideReact.Users
|
|
647
|
-
};
|
|
648
|
-
function editSnapshot(c) {
|
|
649
|
-
return JSON.stringify({
|
|
650
|
-
title: c.title,
|
|
651
|
-
subtitle: c.subtitle,
|
|
652
|
-
description: c.description,
|
|
653
|
-
price: c.price,
|
|
654
|
-
status: c.status,
|
|
655
|
-
deliveryType: c.deliveryType,
|
|
656
|
-
thumbnailUrl: c.thumbnailUrl
|
|
657
|
-
});
|
|
658
|
-
}
|
|
659
|
-
function CourseEditorPage({ id }) {
|
|
660
|
-
const t = core.useTranslation();
|
|
661
|
-
const [tab, setTab] = React3__namespace.useState("details");
|
|
662
|
-
const [course, setCourse] = React3__namespace.useState(null);
|
|
663
|
-
const [modules, setModules] = React3__namespace.useState([]);
|
|
664
|
-
const [lessons, setLessons] = React3__namespace.useState([]);
|
|
665
|
-
const [offers, setOffers] = React3__namespace.useState([]);
|
|
666
|
-
const [students, setStudents] = React3__namespace.useState([]);
|
|
667
|
-
const [loading, setLoading] = React3__namespace.useState(true);
|
|
668
|
-
const [savedSnap, setSavedSnap] = React3__namespace.useState("");
|
|
669
|
-
const [saving, setSaving] = React3__namespace.useState(false);
|
|
670
|
-
const [uploading, setUploading] = React3__namespace.useState(false);
|
|
671
|
-
const fileRef = React3__namespace.useRef(null);
|
|
672
|
-
const [pendingDelete, setPendingDelete] = React3__namespace.useState(null);
|
|
673
|
-
const [autoFocusId, setAutoFocusId] = React3__namespace.useState(null);
|
|
674
|
-
const reload = React3__namespace.useCallback(async () => {
|
|
675
|
-
if (!id) return;
|
|
676
|
-
const provider = courses.getCoursesProvider();
|
|
677
|
-
const c = await provider.getCourse(id);
|
|
678
|
-
setCourse(c);
|
|
679
|
-
if (c) {
|
|
680
|
-
setSavedSnap(editSnapshot(c));
|
|
681
|
-
const [m, l, o, paidOrders] = await Promise.all([
|
|
682
|
-
provider.listModules(c.id),
|
|
683
|
-
provider.listLessons(c.id),
|
|
684
|
-
provider.listOffers(c.id),
|
|
685
|
-
provider.listOrders({ courseId: c.id, financialStatus: "paid" })
|
|
686
|
-
]);
|
|
687
|
-
setModules(m);
|
|
688
|
-
setLessons(l);
|
|
689
|
-
setOffers(o);
|
|
690
|
-
setStudents(buildCourseStudents(paidOrders));
|
|
691
|
-
}
|
|
692
|
-
setLoading(false);
|
|
693
|
-
}, [id]);
|
|
694
|
-
React3__namespace.useEffect(() => {
|
|
695
|
-
void reload();
|
|
696
|
-
}, [reload]);
|
|
697
|
-
const dirty = !!course && editSnapshot(course) !== savedSnap;
|
|
698
|
-
ui.useSaveBar({
|
|
699
|
-
dirty,
|
|
700
|
-
saving,
|
|
701
|
-
onSave: () => {
|
|
702
|
-
void saveDetails();
|
|
703
|
-
},
|
|
704
|
-
onDiscard: () => {
|
|
705
|
-
void reload();
|
|
706
|
-
},
|
|
707
|
-
saveLabel: t("courses.editor.save") || "Salvar"
|
|
708
|
-
});
|
|
709
|
-
if (loading || !course) return /* @__PURE__ */ jsxRuntime.jsx(EditorSkeleton, {});
|
|
710
|
-
function patchCourse(patch) {
|
|
711
|
-
setCourse((c) => c ? { ...c, ...patch } : c);
|
|
712
|
-
}
|
|
713
|
-
async function saveDetails() {
|
|
714
|
-
if (!course) return;
|
|
715
|
-
setSaving(true);
|
|
716
|
-
try {
|
|
717
|
-
await courses.getCoursesProvider().updateCourse(course.id, {
|
|
718
|
-
title: course.title,
|
|
719
|
-
subtitle: course.subtitle ?? void 0,
|
|
720
|
-
description: course.description ?? void 0,
|
|
721
|
-
thumbnailUrl: course.thumbnailUrl ?? void 0,
|
|
722
|
-
price: course.price,
|
|
723
|
-
status: course.status,
|
|
724
|
-
deliveryType: course.deliveryType
|
|
725
|
-
});
|
|
726
|
-
setSavedSnap(editSnapshot(course));
|
|
727
|
-
ui.toast.success(t("courses.editor.saved") || "Course saved");
|
|
728
|
-
} catch {
|
|
729
|
-
ui.toast.error(t("courses.editor.saveFailed") || "Could not save the course");
|
|
730
|
-
} finally {
|
|
731
|
-
setSaving(false);
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
async function onPickThumbnail(e) {
|
|
735
|
-
const file = e.target.files?.[0];
|
|
736
|
-
if (!file || !course) return;
|
|
737
|
-
const supabase = core.getSupabaseClientOptional();
|
|
738
|
-
if (!supabase?.storage) {
|
|
739
|
-
ui.toast.error(t("courses.editor.uploadUnavailable") || "Upload unavailable \u2014 paste an image URL instead.");
|
|
740
|
-
if (fileRef.current) fileRef.current.value = "";
|
|
741
|
-
return;
|
|
742
|
-
}
|
|
743
|
-
setUploading(true);
|
|
744
|
-
try {
|
|
745
|
-
const ext = file.name.split(".").pop() || "jpg";
|
|
746
|
-
const path = `${course.id}/cover-${Date.now()}.${ext}`;
|
|
747
|
-
const bucket = supabase.storage.from("course-media");
|
|
748
|
-
const { error } = await bucket.upload(path, file, { upsert: true, contentType: file.type });
|
|
749
|
-
if (error) throw error;
|
|
750
|
-
const { data } = bucket.getPublicUrl(path);
|
|
751
|
-
patchCourse({ thumbnailUrl: data.publicUrl });
|
|
752
|
-
ui.toast.success(t("courses.editor.thumbnailUploaded") || "Image uploaded \u2014 remember to save.");
|
|
753
|
-
} catch {
|
|
754
|
-
ui.toast.error(t("courses.editor.uploadFailed") || "Could not upload the image");
|
|
755
|
-
} finally {
|
|
756
|
-
setUploading(false);
|
|
757
|
-
if (fileRef.current) fileRef.current.value = "";
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
async function addModule() {
|
|
761
|
-
const mod = await courses.getCoursesProvider().createModule({ courseId: course.id, title: t("courses.editor.untitledModule") || "New module" });
|
|
762
|
-
ui.toast.success(t("courses.editor.moduleAdded") || "Module added");
|
|
763
|
-
await reload();
|
|
764
|
-
setAutoFocusId(mod.id);
|
|
765
|
-
}
|
|
766
|
-
async function renameModule(moduleId, title) {
|
|
767
|
-
await courses.getCoursesProvider().updateModule(moduleId, { title });
|
|
768
|
-
}
|
|
769
|
-
async function addLesson(moduleId) {
|
|
770
|
-
const tempId = `tmp-${crypto.randomUUID()}`;
|
|
771
|
-
const nextOrder = lessons.filter((l) => l.moduleId === moduleId).reduce((max, l) => Math.max(max, l.sortOrder), -1) + 1;
|
|
772
|
-
const optimistic = {
|
|
773
|
-
id: tempId,
|
|
774
|
-
courseId: course.id,
|
|
775
|
-
moduleId,
|
|
776
|
-
title: t("courses.editor.untitledLesson") || "New lesson",
|
|
777
|
-
description: null,
|
|
778
|
-
videoUrl: "",
|
|
779
|
-
durationSec: 0,
|
|
780
|
-
sortOrder: nextOrder
|
|
781
|
-
};
|
|
782
|
-
setLessons((ls) => [...ls, optimistic]);
|
|
783
|
-
setAutoFocusId(tempId);
|
|
784
|
-
try {
|
|
785
|
-
const real = await courses.getCoursesProvider().createLesson({
|
|
786
|
-
courseId: course.id,
|
|
787
|
-
moduleId,
|
|
788
|
-
title: optimistic.title,
|
|
789
|
-
videoUrl: ""
|
|
790
|
-
});
|
|
791
|
-
setLessons((ls) => ls.map((l) => l.id === tempId ? { ...l, id: real.id } : l));
|
|
792
|
-
setAutoFocusId((cur) => cur === tempId ? real.id : cur);
|
|
793
|
-
} catch {
|
|
794
|
-
setLessons((ls) => ls.filter((l) => l.id !== tempId));
|
|
795
|
-
ui.toast.error(t("courses.editor.saveFailed") || "Could not add the lesson");
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
async function patchLesson(lessonId, patch) {
|
|
799
|
-
await courses.getCoursesProvider().updateLesson(lessonId, {
|
|
800
|
-
title: patch.title,
|
|
801
|
-
videoUrl: patch.videoUrl,
|
|
802
|
-
durationSec: patch.durationSec,
|
|
803
|
-
description: patch.description ?? void 0
|
|
804
|
-
});
|
|
805
|
-
}
|
|
806
|
-
async function reorderModules(orderedIds) {
|
|
807
|
-
const prev = modules;
|
|
808
|
-
setModules(orderedIds.map((mid, i) => ({ ...prev.find((m) => m.id === mid), sortOrder: i })));
|
|
809
|
-
try {
|
|
810
|
-
await Promise.all(orderedIds.map((mid, i) => courses.getCoursesProvider().updateModule(mid, { sortOrder: i })));
|
|
811
|
-
} catch {
|
|
812
|
-
ui.toast.error(t("courses.editor.reorderFailed") || "Could not reorder");
|
|
813
|
-
} finally {
|
|
814
|
-
await reload();
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
async function reorderLessons(moduleId, orderedIds) {
|
|
818
|
-
const prev = lessons;
|
|
819
|
-
const others = prev.filter((l) => l.moduleId !== moduleId);
|
|
820
|
-
const reordered = orderedIds.map((lid, i) => ({ ...prev.find((l) => l.id === lid), sortOrder: i }));
|
|
821
|
-
setLessons([...others, ...reordered]);
|
|
822
|
-
try {
|
|
823
|
-
await Promise.all(orderedIds.map((lid, i) => courses.getCoursesProvider().updateLesson(lid, { sortOrder: i })));
|
|
824
|
-
} catch {
|
|
825
|
-
ui.toast.error(t("courses.editor.reorderFailed") || "Could not reorder");
|
|
826
|
-
} finally {
|
|
827
|
-
await reload();
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
async function confirmDelete() {
|
|
831
|
-
if (!pendingDelete) return;
|
|
832
|
-
const provider = courses.getCoursesProvider();
|
|
833
|
-
if (pendingDelete.kind === "module") await provider.deleteModule(pendingDelete.id);
|
|
834
|
-
else await provider.deleteLesson(pendingDelete.id);
|
|
835
|
-
ui.toast.success(t("courses.editor.deleted") || "Removed");
|
|
836
|
-
setPendingDelete(null);
|
|
837
|
-
await reload();
|
|
838
|
-
}
|
|
839
|
-
async function addOffer() {
|
|
840
|
-
await courses.getCoursesProvider().createOffer({
|
|
841
|
-
courseId: course.id,
|
|
842
|
-
name: t("courses.offers.newName") || "New offer",
|
|
843
|
-
price: course.price,
|
|
844
|
-
kind: "one_time"
|
|
845
|
-
});
|
|
846
|
-
ui.toast.success(t("courses.offers.added") || "Offer added");
|
|
847
|
-
await reload();
|
|
848
|
-
}
|
|
849
|
-
async function patchOffer(offerId, patch) {
|
|
850
|
-
await courses.getCoursesProvider().updateOffer(offerId, {
|
|
851
|
-
name: patch.name,
|
|
852
|
-
price: patch.price,
|
|
853
|
-
kind: patch.kind,
|
|
854
|
-
recurringInterval: patch.recurringInterval,
|
|
855
|
-
isOrderBump: patch.isOrderBump,
|
|
856
|
-
isDefault: patch.isDefault
|
|
857
|
-
});
|
|
858
|
-
await reload();
|
|
859
|
-
}
|
|
860
|
-
async function deleteOffer(offerId) {
|
|
861
|
-
await courses.getCoursesProvider().deleteOffer(offerId);
|
|
862
|
-
ui.toast.success(t("courses.offers.removed") || "Offer removed");
|
|
863
|
-
await reload();
|
|
864
|
-
}
|
|
865
|
-
const deliveryOptions = DELIVERY_TYPES.map((d) => ({
|
|
866
|
-
value: d,
|
|
867
|
-
label: t(`courses.editor.delivery.${d}`) || d
|
|
868
|
-
}));
|
|
869
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto max-w-4xl p-6 md:p-8", children: [
|
|
870
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
871
|
-
"button",
|
|
872
|
-
{
|
|
873
|
-
onClick: () => navigateTo("/courses"),
|
|
874
|
-
className: "mb-4 inline-flex items-center gap-1.5 text-sm text-muted-foreground transition-colors hover:text-foreground",
|
|
875
|
-
children: [
|
|
876
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowLeft, { className: "h-4 w-4" }),
|
|
877
|
-
" ",
|
|
878
|
-
t("courses.editor.back") || "Back to courses"
|
|
879
|
-
]
|
|
880
|
-
}
|
|
881
|
-
),
|
|
882
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-6 flex flex-wrap items-center gap-x-3 gap-y-2", children: [
|
|
883
|
-
/* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-2xl font-bold text-foreground", children: course.title || (t("courses.editor.untitledCourse") || "Untitled course") }),
|
|
884
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { variant: course.status === "published" ? "success" : course.status === "archived" ? "secondary" : "outline", children: t(`courses.status.${course.status}`) || course.status }),
|
|
885
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { variant: "outline", children: t(`courses.editor.delivery.${course.deliveryType}`) || course.deliveryType })
|
|
886
|
-
] }),
|
|
887
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.Tabs, { value: tab, onValueChange: (v) => setTab(v), children: [
|
|
888
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "-mx-1 mb-6 overflow-x-auto px-1 pb-1", children: /* @__PURE__ */ jsxRuntime.jsx(ui.TabsList, { className: "h-auto flex-nowrap", children: TABS.map((tk) => {
|
|
889
|
-
const Icon = TAB_ICON[tk];
|
|
890
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(ui.TabsTrigger, { value: tk, className: "gap-1.5 whitespace-nowrap py-1.5", children: [
|
|
891
|
-
/* @__PURE__ */ jsxRuntime.jsx(Icon, { className: "h-3.5 w-3.5" }),
|
|
892
|
-
t(`courses.editor.tab.${tk}`) || tk
|
|
893
|
-
] }, tk);
|
|
894
|
-
}) }) }),
|
|
895
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.TabsContent, { value: "details", className: "space-y-5", children: [
|
|
896
|
-
/* @__PURE__ */ jsxRuntime.jsx(SectionCard, { title: t("courses.editor.sectionBasics") || "Basics", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
|
|
897
|
-
/* @__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 }) }) }),
|
|
898
|
-
/* @__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 }) }) }),
|
|
899
|
-
/* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("courses.editor.fieldDescription") || "Description", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
900
|
-
Textarea,
|
|
901
|
-
{
|
|
902
|
-
value: course.description ?? "",
|
|
903
|
-
onChange: (e) => patchCourse({ description: e.target.value }),
|
|
904
|
-
placeholder: t("courses.editor.descriptionPlaceholder") || "What will students learn?"
|
|
905
|
-
}
|
|
906
|
-
) }),
|
|
907
|
-
/* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("courses.editor.fieldDelivery") || "Format", hint: t("courses.editor.deliveryHint") || "How this course is delivered.", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
908
|
-
ui.SegmentedControl,
|
|
909
|
-
{
|
|
910
|
-
size: "md",
|
|
911
|
-
options: deliveryOptions,
|
|
912
|
-
value: course.deliveryType,
|
|
913
|
-
onChange: (v) => patchCourse({ deliveryType: v }),
|
|
914
|
-
"aria-label": t("courses.editor.fieldDelivery") || "Format"
|
|
915
|
-
}
|
|
916
|
-
) })
|
|
917
|
-
] }) }),
|
|
918
|
-
/* @__PURE__ */ jsxRuntime.jsx(SectionCard, { title: t("courses.editor.sectionThumbnail") || "Cover image", description: t("courses.editor.thumbnailHint") || "Shown on the storefront and in listings. 1280\xD7720 recommended.", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-4 sm:flex-row sm:items-start", children: [
|
|
919
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "aspect-video w-full shrink-0 overflow-hidden rounded-lg border border-border bg-muted sm:w-56", children: course.thumbnailUrl ? (
|
|
920
|
-
// eslint-disable-next-line @next/next/no-img-element
|
|
921
|
-
/* @__PURE__ */ jsxRuntime.jsx("img", { src: course.thumbnailUrl, alt: "", className: "h-full w-full object-cover" })
|
|
922
|
-
) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex h-full w-full flex-col items-center justify-center gap-1 text-muted-foreground", children: [
|
|
923
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.ImagePlus, { className: "h-6 w-6" }),
|
|
924
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs", children: t("courses.editor.thumbnailEmpty") || "No image" })
|
|
925
|
-
] }) }),
|
|
926
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 space-y-3", children: [
|
|
927
|
-
/* @__PURE__ */ jsxRuntime.jsx("input", { ref: fileRef, type: "file", accept: "image/*", className: "hidden", onChange: onPickThumbnail }),
|
|
928
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { variant: "outline", size: "sm", disabled: uploading, onClick: () => fileRef.current?.click(), children: [
|
|
929
|
-
uploading ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.UploadCloud, { className: "h-4 w-4" }),
|
|
930
|
-
uploading ? t("courses.editor.uploading") || "Uploading\u2026" : t("courses.editor.thumbnailUpload") || "Upload image"
|
|
931
|
-
] }),
|
|
932
|
-
/* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("courses.editor.thumbnailUrl") || "Or paste an image URL", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
933
|
-
ui.Input,
|
|
934
|
-
{
|
|
935
|
-
value: course.thumbnailUrl ?? "",
|
|
936
|
-
placeholder: "https://\u2026",
|
|
937
|
-
onChange: (e) => patchCourse({ thumbnailUrl: e.target.value || null })
|
|
938
|
-
}
|
|
939
|
-
) })
|
|
940
|
-
] })
|
|
941
|
-
] }) }),
|
|
942
|
-
/* @__PURE__ */ jsxRuntime.jsx(SectionCard, { title: t("courses.editor.sectionPricing") || "Pricing & status", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2", children: [
|
|
943
|
-
/* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("courses.editor.fieldPrice") || "Price", hint: course.currency, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
944
|
-
ui.Input,
|
|
945
|
-
{
|
|
946
|
-
type: "number",
|
|
947
|
-
min: 0,
|
|
948
|
-
step: "0.01",
|
|
949
|
-
inputMode: "decimal",
|
|
950
|
-
value: String(course.price),
|
|
951
|
-
onChange: (e) => patchCourse({ price: Number(e.target.value) || 0 })
|
|
952
|
-
}
|
|
953
|
-
) }),
|
|
954
|
-
/* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("courses.editor.fieldStatus") || "Status", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Select, { value: course.status, onValueChange: (v) => patchCourse({ status: v }), children: [
|
|
955
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.SelectTrigger, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.SelectValue, {}) }),
|
|
956
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.SelectContent, { children: STATUSES.map((s) => /* @__PURE__ */ jsxRuntime.jsx(ui.SelectItem, { value: s, children: t(`courses.status.${s}`) || s }, s)) })
|
|
957
|
-
] }) })
|
|
958
|
-
] }) })
|
|
959
|
-
] }),
|
|
960
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.TabsContent, { value: "curriculum", className: "space-y-4", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
961
|
-
CurriculumBuilder,
|
|
962
|
-
{
|
|
963
|
-
t,
|
|
964
|
-
modules,
|
|
965
|
-
lessons,
|
|
966
|
-
autoFocusId,
|
|
967
|
-
onAutoFocused: () => setAutoFocusId(null),
|
|
968
|
-
onAddModule: addModule,
|
|
969
|
-
onRenameModule: renameModule,
|
|
970
|
-
onAddLesson: addLesson,
|
|
971
|
-
onPatchLesson: patchLesson,
|
|
972
|
-
onRequestDelete: (kind, delId) => setPendingDelete({ kind, id: delId }),
|
|
973
|
-
onReorderModules: reorderModules,
|
|
974
|
-
onReorderLessons: reorderLessons
|
|
975
|
-
}
|
|
976
|
-
) }),
|
|
977
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.TabsContent, { value: "offers", className: "space-y-4", children: [
|
|
978
|
-
/* @__PURE__ */ jsxRuntime.jsx(InfoNote, { icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Info, { className: "h-4 w-4" }), children: t("courses.offers.help") || "Price points for this course. Sales run through orders and the Financeiro module." }),
|
|
979
|
-
offers.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
980
|
-
EmptyState,
|
|
981
|
-
{
|
|
982
|
-
icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Tag, { className: "h-5 w-5" }),
|
|
983
|
-
title: t("courses.offers.empty") || "No offers yet",
|
|
984
|
-
description: t("courses.offers.emptyHint") || "Create an offer to sell this course.",
|
|
985
|
-
action: /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { onClick: addOffer, children: [
|
|
986
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-4 w-4" }),
|
|
987
|
-
t("courses.offers.firstOffer") || "Create first offer"
|
|
988
|
-
] })
|
|
989
|
-
}
|
|
990
|
-
) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
991
|
-
offers.map((offer) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-xl border border-border bg-card p-4 shadow-sm md:p-5", children: [
|
|
992
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-4 flex items-start justify-between gap-3", children: [
|
|
993
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { variant: "outline", className: "gap-1", children: offer.kind === "subscription" ? t("courses.offers.subscription") || "Subscription" : t("courses.offers.oneTime") || "One-time" }),
|
|
994
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
995
|
-
"button",
|
|
996
|
-
{
|
|
997
|
-
onClick: () => deleteOffer(offer.id),
|
|
998
|
-
className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-muted-foreground/60 transition-colors hover:bg-destructive/10 hover:text-destructive",
|
|
999
|
-
"aria-label": t("courses.offers.remove") || "Remove",
|
|
1000
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { className: "h-4 w-4" })
|
|
1001
|
-
}
|
|
1002
|
-
)
|
|
1003
|
-
] }),
|
|
1004
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-[1fr_140px_160px]", children: [
|
|
1005
|
-
/* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("courses.offers.name") || "Offer name", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { defaultValue: offer.name, onBlur: (e) => patchOffer(offer.id, { name: e.target.value }) }) }),
|
|
1006
|
-
/* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("courses.offers.price") || "Price", hint: offer.currency, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Input, { type: "number", min: 0, step: "0.01", inputMode: "decimal", defaultValue: String(offer.price), onBlur: (e) => patchOffer(offer.id, { price: Number(e.target.value) || 0 }) }) }),
|
|
1007
|
-
/* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("courses.offers.kind") || "Type", children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1008
|
-
ui.Select,
|
|
1009
|
-
{
|
|
1010
|
-
value: offer.kind,
|
|
1011
|
-
onValueChange: (v) => patchOffer(offer.id, { kind: v, recurringInterval: v === "subscription" ? offer.recurringInterval ?? "month" : null }),
|
|
1012
|
-
children: [
|
|
1013
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.SelectTrigger, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.SelectValue, {}) }),
|
|
1014
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.SelectContent, { children: [
|
|
1015
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.SelectItem, { value: "one_time", children: t("courses.offers.oneTime") || "One-time" }),
|
|
1016
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.SelectItem, { value: "subscription", children: t("courses.offers.subscription") || "Subscription" })
|
|
1017
|
-
] })
|
|
1018
|
-
]
|
|
1019
|
-
}
|
|
1020
|
-
) })
|
|
1021
|
-
] }),
|
|
1022
|
-
offer.kind === "subscription" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-3 max-w-[200px]", children: /* @__PURE__ */ jsxRuntime.jsx(Field, { label: t("courses.offers.interval") || "Billing interval", children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1023
|
-
ui.Select,
|
|
1024
|
-
{
|
|
1025
|
-
value: offer.recurringInterval ?? "month",
|
|
1026
|
-
onValueChange: (v) => patchOffer(offer.id, { recurringInterval: v }),
|
|
1027
|
-
children: [
|
|
1028
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.SelectTrigger, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.SelectValue, {}) }),
|
|
1029
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.SelectContent, { children: [
|
|
1030
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.SelectItem, { value: "month", children: t("courses.interval.month") || "month" }),
|
|
1031
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.SelectItem, { value: "year", children: t("courses.interval.year") || "year" })
|
|
1032
|
-
] })
|
|
1033
|
-
]
|
|
1034
|
-
}
|
|
1035
|
-
) }) }),
|
|
1036
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-4 rounded-lg border border-dashed border-border bg-muted/30 p-3", children: [
|
|
1037
|
-
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "mb-2.5 flex items-center gap-1.5 text-xs text-muted-foreground", children: [
|
|
1038
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Clock, { className: "h-3.5 w-3.5" }),
|
|
1039
|
-
t("courses.offers.checkoutFlagsNote") || "Apply once the public checkout ships (coming soon)."
|
|
1040
|
-
] }),
|
|
1041
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-x-5 gap-y-2", children: [
|
|
1042
|
-
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex cursor-pointer items-center gap-2 text-sm text-muted-foreground", children: [
|
|
1043
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Checkbox, { checked: offer.isDefault, onChange: (v) => patchOffer(offer.id, { isDefault: v }) }),
|
|
1044
|
-
t("courses.offers.default") || "Default"
|
|
1045
|
-
] }),
|
|
1046
|
-
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex cursor-pointer items-center gap-2 text-sm text-muted-foreground", children: [
|
|
1047
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Checkbox, { checked: offer.isOrderBump, onChange: (v) => patchOffer(offer.id, { isOrderBump: v }) }),
|
|
1048
|
-
t("courses.offers.bump") || "Order bump"
|
|
1049
|
-
] })
|
|
1050
|
-
] })
|
|
1051
|
-
] })
|
|
1052
|
-
] }, offer.id)),
|
|
1053
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { variant: "outline", onClick: addOffer, className: "w-full border-dashed", children: [
|
|
1054
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-4 w-4" }),
|
|
1055
|
-
t("courses.offers.add") || "Add offer"
|
|
1056
|
-
] })
|
|
1057
|
-
] })
|
|
1058
|
-
] }),
|
|
1059
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.TabsContent, { value: "members", className: "space-y-4", children: [
|
|
1060
|
-
/* @__PURE__ */ jsxRuntime.jsx(InfoNote, { icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Users, { className: "h-4 w-4" }), children: t("courses.editor.membersHelp") || "Buyers are enrolled automatically through orders and get access to all of this course\u2019s content." }),
|
|
1061
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
|
|
1062
|
-
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: [
|
|
1063
|
-
t("courses.editor.enrolledStudents") || "Enrolled students",
|
|
1064
|
-
" (",
|
|
1065
|
-
students.length,
|
|
1066
|
-
")"
|
|
1067
|
-
] }),
|
|
1068
|
-
students.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1069
|
-
EmptyState,
|
|
1070
|
-
{
|
|
1071
|
-
icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Users, { className: "h-5 w-5" }),
|
|
1072
|
-
title: t("courses.editor.membersEmpty") || "No students yet",
|
|
1073
|
-
description: t("courses.editor.membersEmptyHint") || "Buyers are enrolled here automatically after a paid order."
|
|
1074
|
-
}
|
|
1075
|
-
) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "divide-y divide-border overflow-hidden rounded-xl border border-border bg-card", children: students.map((s) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 px-4 py-3", children: [
|
|
1076
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted text-xs font-semibold uppercase text-muted-foreground", "aria-hidden": true, children: (s.name || s.email || "?").charAt(0) }),
|
|
1077
|
-
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [
|
|
1078
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate font-medium text-foreground", children: s.name }),
|
|
1079
|
-
s.email && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-xs text-muted-foreground", children: s.email })
|
|
1080
|
-
] }),
|
|
1081
|
-
s.since && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "shrink-0 text-xs tabular-nums text-muted-foreground", children: new Date(s.since).toLocaleDateString() })
|
|
1082
|
-
] }, s.key)) })
|
|
1083
|
-
] }),
|
|
1084
|
-
offers.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
|
|
1085
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: t("courses.editor.accessUnlockedBy") || "Access is unlocked by" }),
|
|
1086
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "divide-y divide-border overflow-hidden rounded-xl border border-border bg-card", children: offers.map((offer) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 px-4 py-3", children: [
|
|
1087
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground", "aria-hidden": true, children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Lock, { className: "h-4 w-4" }) }),
|
|
1088
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 flex-1 truncate font-medium text-foreground", children: offer.name }),
|
|
1089
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "shrink-0 text-sm tabular-nums text-muted-foreground", children: formatMoney(offer.price, offer.currency) })
|
|
1090
|
-
] }, offer.id)) })
|
|
1091
|
-
] })
|
|
1092
|
-
] })
|
|
1093
|
-
] }),
|
|
1094
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1095
|
-
ui.ConfirmDialog,
|
|
1096
|
-
{
|
|
1097
|
-
open: pendingDelete !== null,
|
|
1098
|
-
variant: "destructive",
|
|
1099
|
-
title: pendingDelete?.kind === "module" ? t("courses.editor.deleteModuleConfirm") || "Delete this module?" : t("courses.editor.deleteLessonConfirm") || "Remove this lesson?",
|
|
1100
|
-
description: pendingDelete?.kind === "module" ? t("courses.editor.deleteModuleConfirmBody") || "Its lessons will be removed too. This cannot be undone." : t("courses.editor.deleteLessonConfirmBody") || "This cannot be undone.",
|
|
1101
|
-
confirmLabel: t("courses.editor.confirmDelete") || "Delete",
|
|
1102
|
-
cancelLabel: t("courses.editor.cancel") || "Cancel",
|
|
1103
|
-
onConfirm: confirmDelete,
|
|
1104
|
-
onCancel: () => setPendingDelete(null)
|
|
1105
|
-
}
|
|
1106
|
-
)
|
|
1107
|
-
] });
|
|
1108
|
-
}
|
|
1109
|
-
CourseEditorPage.displayName = "CourseEditorPage";
|
|
1110
|
-
function formatDuration(sec, t) {
|
|
1111
|
-
const min = Math.max(0, Math.round(sec / 60));
|
|
1112
|
-
const mLabel = t("courses.editor.minShort") || "min";
|
|
1113
|
-
if (min < 60) return `${min} ${mLabel}`;
|
|
1114
|
-
const h = Math.floor(min / 60);
|
|
1115
|
-
const rem = min % 60;
|
|
1116
|
-
const hLabel = t("courses.editor.hourShort") || "h";
|
|
1117
|
-
return rem ? `${h} ${hLabel} ${rem} ${mLabel}` : `${h} ${hLabel}`;
|
|
1118
|
-
}
|
|
1119
|
-
function CurriculumBuilder({
|
|
1120
|
-
t,
|
|
1121
|
-
modules,
|
|
1122
|
-
lessons,
|
|
1123
|
-
autoFocusId,
|
|
1124
|
-
onAutoFocused,
|
|
1125
|
-
onAddModule,
|
|
1126
|
-
onRenameModule,
|
|
1127
|
-
onAddLesson,
|
|
1128
|
-
onPatchLesson,
|
|
1129
|
-
onRequestDelete,
|
|
1130
|
-
onReorderModules,
|
|
1131
|
-
onReorderLessons
|
|
1132
|
-
}) {
|
|
1133
|
-
const [collapsed, setCollapsed] = React3__namespace.useState(/* @__PURE__ */ new Set());
|
|
1134
|
-
const sensors = core$1.useSensors(
|
|
1135
|
-
core$1.useSensor(core$1.PointerSensor, { activationConstraint: { distance: 6 } }),
|
|
1136
|
-
core$1.useSensor(core$1.KeyboardSensor, { coordinateGetter: sortable.sortableKeyboardCoordinates })
|
|
1137
|
-
);
|
|
1138
|
-
function toggleModule(id) {
|
|
1139
|
-
setCollapsed((prev) => {
|
|
1140
|
-
const next = new Set(prev);
|
|
1141
|
-
if (next.has(id)) next.delete(id);
|
|
1142
|
-
else next.add(id);
|
|
1143
|
-
return next;
|
|
1144
|
-
});
|
|
1145
|
-
}
|
|
1146
|
-
function handleModuleDragEnd(e) {
|
|
1147
|
-
const { active, over } = e;
|
|
1148
|
-
if (!over || active.id === over.id) return;
|
|
1149
|
-
const ids = modules.map((m) => m.id);
|
|
1150
|
-
const from = ids.indexOf(String(active.id));
|
|
1151
|
-
const to = ids.indexOf(String(over.id));
|
|
1152
|
-
if (from < 0 || to < 0) return;
|
|
1153
|
-
onReorderModules(sortable.arrayMove(ids, from, to));
|
|
1154
|
-
}
|
|
1155
|
-
if (modules.length === 0) {
|
|
1156
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
|
|
1157
|
-
/* @__PURE__ */ jsxRuntime.jsx(AutosaveHint, { t }),
|
|
1158
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1159
|
-
EmptyState,
|
|
1160
|
-
{
|
|
1161
|
-
icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Layers, { className: "h-5 w-5" }),
|
|
1162
|
-
title: t("courses.editor.modulesEmpty") || "No modules yet",
|
|
1163
|
-
description: t("courses.editor.modulesEmptyHint") || "Create a module to start organizing your lessons.",
|
|
1164
|
-
action: /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { onClick: onAddModule, children: [
|
|
1165
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-4 w-4" }),
|
|
1166
|
-
t("courses.editor.firstModule") || "Create first module"
|
|
1167
|
-
] })
|
|
1168
|
-
}
|
|
1169
|
-
)
|
|
1170
|
-
] });
|
|
1171
|
-
}
|
|
1172
|
-
const totalLessons = lessons.length;
|
|
1173
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
|
|
1174
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [
|
|
1175
|
-
/* @__PURE__ */ jsxRuntime.jsx(AutosaveHint, { t }),
|
|
1176
|
-
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-muted-foreground tabular-nums", children: [
|
|
1177
|
-
modules.length,
|
|
1178
|
-
" ",
|
|
1179
|
-
modules.length === 1 ? t("courses.editor.moduleLabel") || "module" : t("courses.editor.moduleLabelPlural") || "modules",
|
|
1180
|
-
" \xB7 ",
|
|
1181
|
-
totalLessons,
|
|
1182
|
-
" ",
|
|
1183
|
-
totalLessons === 1 ? t("courses.editor.lessonUnitOne") || "lesson" : t("courses.editor.lessonUnitOther") || "lessons"
|
|
1184
|
-
] })
|
|
1185
|
-
] }),
|
|
1186
|
-
/* @__PURE__ */ jsxRuntime.jsx(core$1.DndContext, { sensors, collisionDetection: core$1.closestCenter, onDragEnd: handleModuleDragEnd, children: /* @__PURE__ */ jsxRuntime.jsx(sortable.SortableContext, { items: modules.map((m) => m.id), strategy: sortable.verticalListSortingStrategy, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-3", children: modules.map((mod, i) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1187
|
-
SortableModuleCard,
|
|
1188
|
-
{
|
|
1189
|
-
t,
|
|
1190
|
-
index: i,
|
|
1191
|
-
mod,
|
|
1192
|
-
lessons: lessons.filter((l) => l.moduleId === mod.id),
|
|
1193
|
-
expanded: !collapsed.has(mod.id),
|
|
1194
|
-
onToggle: () => toggleModule(mod.id),
|
|
1195
|
-
autoFocusId,
|
|
1196
|
-
onAutoFocused,
|
|
1197
|
-
onRenameModule,
|
|
1198
|
-
onAddLesson,
|
|
1199
|
-
onPatchLesson,
|
|
1200
|
-
onRequestDelete,
|
|
1201
|
-
onReorderLessons
|
|
1202
|
-
},
|
|
1203
|
-
mod.id
|
|
1204
|
-
)) }) }) }),
|
|
1205
|
-
/* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { variant: "outline", onClick: onAddModule, className: "w-full border-dashed", children: [
|
|
1206
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-4 w-4" }),
|
|
1207
|
-
t("courses.editor.newModule") || "New module"
|
|
1208
|
-
] })
|
|
1209
|
-
] });
|
|
1210
|
-
}
|
|
1211
|
-
function SortableModuleCard({
|
|
1212
|
-
t,
|
|
1213
|
-
index,
|
|
1214
|
-
mod,
|
|
1215
|
-
lessons,
|
|
1216
|
-
expanded,
|
|
1217
|
-
onToggle,
|
|
1218
|
-
autoFocusId,
|
|
1219
|
-
onAutoFocused,
|
|
1220
|
-
onRenameModule,
|
|
1221
|
-
onAddLesson,
|
|
1222
|
-
onPatchLesson,
|
|
1223
|
-
onRequestDelete,
|
|
1224
|
-
onReorderLessons
|
|
1225
|
-
}) {
|
|
1226
|
-
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = sortable.useSortable({ id: mod.id });
|
|
1227
|
-
const sensors = core$1.useSensors(
|
|
1228
|
-
core$1.useSensor(core$1.PointerSensor, { activationConstraint: { distance: 6 } }),
|
|
1229
|
-
core$1.useSensor(core$1.KeyboardSensor, { coordinateGetter: sortable.sortableKeyboardCoordinates })
|
|
1230
|
-
);
|
|
1231
|
-
const style = {
|
|
1232
|
-
transform: utilities.CSS.Transform.toString(transform),
|
|
1233
|
-
transition,
|
|
1234
|
-
opacity: isDragging ? 0.6 : 1,
|
|
1235
|
-
zIndex: isDragging ? 20 : void 0
|
|
1236
|
-
};
|
|
1237
|
-
const count = lessons.length;
|
|
1238
|
-
const totalSec = lessons.reduce((s, l) => s + (l.durationSec || 0), 0);
|
|
1239
|
-
function handleLessonDragEnd(e) {
|
|
1240
|
-
const { active, over } = e;
|
|
1241
|
-
if (!over || active.id === over.id) return;
|
|
1242
|
-
const ids = lessons.map((l) => l.id);
|
|
1243
|
-
const from = ids.indexOf(String(active.id));
|
|
1244
|
-
const to = ids.indexOf(String(over.id));
|
|
1245
|
-
if (from < 0 || to < 0) return;
|
|
1246
|
-
onReorderLessons(mod.id, sortable.arrayMove(ids, from, to));
|
|
1247
|
-
}
|
|
1248
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1249
|
-
"div",
|
|
1250
|
-
{
|
|
1251
|
-
ref: setNodeRef,
|
|
1252
|
-
style,
|
|
1253
|
-
className: "overflow-hidden rounded-xl border border-border bg-card shadow-sm",
|
|
1254
|
-
children: [
|
|
1255
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 px-2 py-2 sm:gap-2 sm:px-3", children: [
|
|
1256
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1257
|
-
"button",
|
|
1258
|
-
{
|
|
1259
|
-
...attributes,
|
|
1260
|
-
...listeners,
|
|
1261
|
-
className: "hidden h-8 w-6 shrink-0 cursor-grab touch-none items-center justify-center rounded-md text-muted-foreground/40 transition-colors hover:text-muted-foreground active:cursor-grabbing sm:flex",
|
|
1262
|
-
"aria-label": t("courses.editor.dragToReorder") || "Drag to reorder",
|
|
1263
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.GripVertical, { className: "h-4 w-4" })
|
|
1264
|
-
}
|
|
1265
|
-
),
|
|
1266
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1267
|
-
"button",
|
|
1268
|
-
{
|
|
1269
|
-
onClick: onToggle,
|
|
1270
|
-
className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
|
|
1271
|
-
"aria-label": expanded ? t("courses.editor.collapse") || "Collapse" : t("courses.editor.expand") || "Expand",
|
|
1272
|
-
"aria-expanded": expanded,
|
|
1273
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, { className: "h-4 w-4 transition-transform " + (expanded ? "" : "-rotate-90") })
|
|
1274
|
-
}
|
|
1275
|
-
),
|
|
1276
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden shrink-0 items-center rounded-md bg-muted px-2 py-1 text-xs font-semibold tabular-nums text-muted-foreground sm:inline-flex", children: String(index + 1).padStart(2, "0") }),
|
|
1277
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1278
|
-
TitleField,
|
|
1279
|
-
{
|
|
1280
|
-
value: mod.title,
|
|
1281
|
-
autoFocus: autoFocusId === mod.id,
|
|
1282
|
-
onAutoFocused,
|
|
1283
|
-
onCommit: (v) => onRenameModule(mod.id, v),
|
|
1284
|
-
placeholder: t("courses.editor.moduleTitle") || "Module title",
|
|
1285
|
-
ariaLabel: t("courses.editor.moduleTitle") || "Module title",
|
|
1286
|
-
className: "text-sm font-semibold"
|
|
1287
|
-
}
|
|
1288
|
-
),
|
|
1289
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden shrink-0 text-xs text-muted-foreground tabular-nums md:inline", children: count > 0 ? `${count} ${count === 1 ? t("courses.editor.lessonUnitOne") || "lesson" : t("courses.editor.lessonUnitOther") || "lessons"} \xB7 ${formatDuration(totalSec, t)}` : t("courses.editor.noLessons") || "No lessons" }),
|
|
1290
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1291
|
-
"button",
|
|
1292
|
-
{
|
|
1293
|
-
onClick: () => onRequestDelete("module", mod.id),
|
|
1294
|
-
className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-muted-foreground/60 transition-colors hover:bg-destructive/10 hover:text-destructive",
|
|
1295
|
-
"aria-label": t("courses.editor.deleteModule") || "Delete module",
|
|
1296
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { className: "h-4 w-4" })
|
|
1297
|
-
}
|
|
1298
|
-
)
|
|
1299
|
-
] }),
|
|
1300
|
-
expanded && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-t border-border/60 bg-muted/20 p-2 sm:p-3", children: [
|
|
1301
|
-
count === 0 ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1302
|
-
"button",
|
|
1303
|
-
{
|
|
1304
|
-
onClick: () => onAddLesson(mod.id),
|
|
1305
|
-
className: "flex w-full items-center justify-center gap-2 rounded-lg border border-dashed border-border px-3 py-4 text-sm text-muted-foreground transition-colors hover:border-primary/40 hover:bg-card hover:text-foreground",
|
|
1306
|
-
children: [
|
|
1307
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-4 w-4" }),
|
|
1308
|
-
" ",
|
|
1309
|
-
t("courses.editor.firstLesson") || "Add the first lesson"
|
|
1310
|
-
]
|
|
1311
|
-
}
|
|
1312
|
-
) : /* @__PURE__ */ jsxRuntime.jsx(core$1.DndContext, { sensors, collisionDetection: core$1.closestCenter, onDragEnd: handleLessonDragEnd, children: /* @__PURE__ */ jsxRuntime.jsx(sortable.SortableContext, { items: lessons.map((l) => l.id), strategy: sortable.verticalListSortingStrategy, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-1.5", children: lessons.map((lesson) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1313
|
-
SortableLessonRow,
|
|
1314
|
-
{
|
|
1315
|
-
t,
|
|
1316
|
-
lesson,
|
|
1317
|
-
autoFocusId,
|
|
1318
|
-
onAutoFocused,
|
|
1319
|
-
onPatchLesson,
|
|
1320
|
-
onRequestDelete
|
|
1321
|
-
},
|
|
1322
|
-
lesson.id
|
|
1323
|
-
)) }) }) }),
|
|
1324
|
-
count > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1325
|
-
"button",
|
|
1326
|
-
{
|
|
1327
|
-
onClick: () => onAddLesson(mod.id),
|
|
1328
|
-
className: "mt-2 flex items-center gap-1.5 rounded-md px-2 py-1.5 text-sm font-medium text-primary transition-colors hover:bg-primary/10",
|
|
1329
|
-
children: [
|
|
1330
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-4 w-4" }),
|
|
1331
|
-
" ",
|
|
1332
|
-
t("courses.editor.newLesson") || "New lesson"
|
|
1333
|
-
]
|
|
1334
|
-
}
|
|
1335
|
-
)
|
|
1336
|
-
] })
|
|
1337
|
-
]
|
|
1338
|
-
}
|
|
1339
|
-
);
|
|
1340
|
-
}
|
|
1341
|
-
function SortableLessonRow({ t, lesson, autoFocusId, onAutoFocused, onPatchLesson, onRequestDelete }) {
|
|
1342
|
-
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = sortable.useSortable({ id: lesson.id });
|
|
1343
|
-
const [open, setOpen] = React3__namespace.useState(false);
|
|
1344
|
-
const [videoUrl, setVideoUrl] = React3__namespace.useState(lesson.videoUrl);
|
|
1345
|
-
const [durationSec, setDurationSec] = React3__namespace.useState(lesson.durationSec);
|
|
1346
|
-
React3__namespace.useEffect(() => {
|
|
1347
|
-
setVideoUrl(lesson.videoUrl);
|
|
1348
|
-
}, [lesson.videoUrl]);
|
|
1349
|
-
React3__namespace.useEffect(() => {
|
|
1350
|
-
setDurationSec(lesson.durationSec);
|
|
1351
|
-
}, [lesson.durationSec]);
|
|
1352
|
-
const hasVideo = videoUrl.trim().length > 0;
|
|
1353
|
-
const style = {
|
|
1354
|
-
transform: utilities.CSS.Transform.toString(transform),
|
|
1355
|
-
transition,
|
|
1356
|
-
opacity: isDragging ? 0.6 : 1,
|
|
1357
|
-
zIndex: isDragging ? 10 : void 0
|
|
1358
|
-
};
|
|
1359
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1360
|
-
"div",
|
|
1361
|
-
{
|
|
1362
|
-
ref: setNodeRef,
|
|
1363
|
-
style,
|
|
1364
|
-
className: "group rounded-lg border bg-card transition-colors " + (open ? "border-border" : "border-transparent hover:border-border"),
|
|
1365
|
-
children: [
|
|
1366
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 px-1.5 py-1.5 sm:gap-2 sm:px-2", children: [
|
|
1367
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1368
|
-
"button",
|
|
1369
|
-
{
|
|
1370
|
-
...attributes,
|
|
1371
|
-
...listeners,
|
|
1372
|
-
className: "hidden h-8 w-5 shrink-0 cursor-grab touch-none items-center justify-center text-muted-foreground/40 transition-colors hover:text-muted-foreground active:cursor-grabbing sm:flex",
|
|
1373
|
-
"aria-label": t("courses.editor.dragToReorder") || "Drag to reorder",
|
|
1374
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.GripVertical, { className: "h-4 w-4" })
|
|
1375
|
-
}
|
|
1376
|
-
),
|
|
1377
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1378
|
-
"span",
|
|
1379
|
-
{
|
|
1380
|
-
className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-lg " + (hasVideo ? "bg-primary/10 text-primary" : "bg-muted text-muted-foreground"),
|
|
1381
|
-
"aria-hidden": true,
|
|
1382
|
-
children: hasVideo ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Video, { className: "h-4 w-4" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileText, { className: "h-4 w-4" })
|
|
1383
|
-
}
|
|
1384
|
-
),
|
|
1385
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1386
|
-
TitleField,
|
|
1387
|
-
{
|
|
1388
|
-
value: lesson.title,
|
|
1389
|
-
autoFocus: autoFocusId === lesson.id,
|
|
1390
|
-
onAutoFocused,
|
|
1391
|
-
onCommit: (v) => onPatchLesson(lesson.id, { title: v }),
|
|
1392
|
-
placeholder: t("courses.editor.lessonTitle") || "Lesson title",
|
|
1393
|
-
ariaLabel: t("courses.editor.lessonTitle") || "Lesson title",
|
|
1394
|
-
className: "text-sm"
|
|
1395
|
-
}
|
|
1396
|
-
),
|
|
1397
|
-
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "hidden shrink-0 items-center gap-1 rounded-full bg-muted px-2 py-0.5 text-xs tabular-nums text-muted-foreground sm:inline-flex", children: [
|
|
1398
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Clock, { className: "h-3 w-3" }),
|
|
1399
|
-
Math.round(durationSec / 60),
|
|
1400
|
-
" ",
|
|
1401
|
-
t("courses.editor.minShort") || "min"
|
|
1402
|
-
] }),
|
|
1403
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1404
|
-
"button",
|
|
1405
|
-
{
|
|
1406
|
-
onClick: () => setOpen((v) => !v),
|
|
1407
|
-
className: "inline-flex shrink-0 items-center gap-1 rounded-full border px-2 py-1 text-xs font-medium transition-colors " + (hasVideo ? "border-border text-muted-foreground hover:text-foreground" : "border-dashed border-border text-muted-foreground hover:border-primary/40 hover:text-foreground") + " opacity-100 sm:opacity-0 sm:group-hover:opacity-100 " + (open ? "sm:opacity-100" : ""),
|
|
1408
|
-
"aria-label": t("courses.editor.editLesson") || "Edit lesson",
|
|
1409
|
-
"aria-expanded": open,
|
|
1410
|
-
children: [
|
|
1411
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.PlayCircle, { className: "h-3.5 w-3.5" }),
|
|
1412
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: hasVideo ? t("courses.editor.videoSet") || "Video" : t("courses.editor.addVideo") || "Add video" })
|
|
1413
|
-
]
|
|
1414
|
-
}
|
|
1415
|
-
),
|
|
1416
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1417
|
-
"button",
|
|
1418
|
-
{
|
|
1419
|
-
onClick: () => onRequestDelete("lesson", lesson.id),
|
|
1420
|
-
className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-muted-foreground/60 transition-colors hover:bg-destructive/10 hover:text-destructive sm:opacity-0 sm:group-hover:opacity-100",
|
|
1421
|
-
"aria-label": t("courses.editor.deleteLesson") || "Remove lesson",
|
|
1422
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { className: "h-4 w-4" })
|
|
1423
|
-
}
|
|
1424
|
-
)
|
|
1425
|
-
] }),
|
|
1426
|
-
open && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3 border-t border-border/60 px-3 py-3 sm:pl-11", children: [
|
|
1427
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1.5", children: [
|
|
1428
|
-
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("courses.editor.videoUrlLabel") || "Video URL" }),
|
|
1429
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1430
|
-
ui.Input,
|
|
1431
|
-
{
|
|
1432
|
-
defaultValue: lesson.videoUrl,
|
|
1433
|
-
placeholder: t("courses.editor.videoUrlPlaceholder") || "Paste the video link (YouTube, Vimeo\u2026)",
|
|
1434
|
-
onBlur: (e) => {
|
|
1435
|
-
setVideoUrl(e.target.value);
|
|
1436
|
-
onPatchLesson(lesson.id, { videoUrl: e.target.value });
|
|
1437
|
-
},
|
|
1438
|
-
"aria-label": t("courses.editor.videoUrlLabel") || "Video URL"
|
|
1439
|
-
}
|
|
1440
|
-
)
|
|
1441
|
-
] }),
|
|
1442
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "max-w-[160px] space-y-1.5", children: [
|
|
1443
|
-
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("courses.editor.lessonDuration") || "Duration (min)" }),
|
|
1444
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1445
|
-
ui.Input,
|
|
1446
|
-
{
|
|
1447
|
-
type: "number",
|
|
1448
|
-
min: 0,
|
|
1449
|
-
inputMode: "numeric",
|
|
1450
|
-
defaultValue: String(Math.round(lesson.durationSec / 60)),
|
|
1451
|
-
onBlur: (e) => {
|
|
1452
|
-
const s = (Number(e.target.value) || 0) * 60;
|
|
1453
|
-
setDurationSec(s);
|
|
1454
|
-
onPatchLesson(lesson.id, { durationSec: s });
|
|
1455
|
-
},
|
|
1456
|
-
"aria-label": t("courses.editor.lessonDuration") || "Duration (min)"
|
|
1457
|
-
}
|
|
1458
|
-
)
|
|
1459
|
-
] })
|
|
1460
|
-
] })
|
|
1461
|
-
]
|
|
1462
|
-
}
|
|
1463
|
-
);
|
|
1464
|
-
}
|
|
1465
|
-
function TitleField({ value, autoFocus, onAutoFocused, onCommit, placeholder, ariaLabel, className }) {
|
|
1466
|
-
const ref = React3__namespace.useRef(null);
|
|
1467
|
-
React3__namespace.useEffect(() => {
|
|
1468
|
-
if (autoFocus && ref.current) {
|
|
1469
|
-
ref.current.focus();
|
|
1470
|
-
ref.current.select();
|
|
1471
|
-
onAutoFocused?.();
|
|
1472
|
-
}
|
|
1473
|
-
}, [autoFocus]);
|
|
1474
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1475
|
-
"input",
|
|
1476
|
-
{
|
|
1477
|
-
ref,
|
|
1478
|
-
defaultValue: value,
|
|
1479
|
-
placeholder,
|
|
1480
|
-
"aria-label": ariaLabel,
|
|
1481
|
-
onBlur: (e) => onCommit(e.target.value),
|
|
1482
|
-
onKeyDown: (e) => {
|
|
1483
|
-
if (e.key === "Enter") e.currentTarget.blur();
|
|
1484
|
-
},
|
|
1485
|
-
className: "min-w-0 flex-1 rounded-md bg-transparent px-2 py-1.5 text-foreground outline-none transition-colors placeholder:text-muted-foreground/60 hover:bg-muted/60 focus:bg-background focus:ring-2 focus:ring-ring " + (className ?? "")
|
|
1486
|
-
}
|
|
1487
|
-
);
|
|
1488
|
-
}
|
|
1489
|
-
function Field({ label, hint, children }) {
|
|
1490
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1.5", children: [
|
|
1491
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-baseline justify-between gap-2", children: [
|
|
1492
|
-
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-sm font-medium text-foreground", children: label }),
|
|
1493
|
-
hint && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground", children: hint })
|
|
1494
|
-
] }),
|
|
1495
|
-
children
|
|
1496
|
-
] });
|
|
1497
|
-
}
|
|
1498
|
-
function Textarea(props) {
|
|
1499
|
-
const { className, ...rest } = props;
|
|
1500
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1501
|
-
"textarea",
|
|
1502
|
-
{
|
|
1503
|
-
className: "flex min-h-[96px] w-full rounded-input border border-input bg-card px-2 py-1.5 text-sm text-foreground shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 " + (className ?? ""),
|
|
1504
|
-
...rest
|
|
1505
|
-
}
|
|
1506
|
-
);
|
|
1507
|
-
}
|
|
1508
|
-
function AutosaveHint({ t }) {
|
|
1509
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [
|
|
1510
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { className: "h-3.5 w-3.5 text-success" }),
|
|
1511
|
-
t("courses.editor.autosaved") || "Changes are saved automatically."
|
|
1512
|
-
] });
|
|
1513
|
-
}
|
|
1514
|
-
function EditorSkeleton() {
|
|
1515
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto max-w-4xl p-6 md:p-8", children: [
|
|
1516
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "mb-4 h-4 w-28" }),
|
|
1517
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "mb-6 h-8 w-64" }),
|
|
1518
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "mb-6 h-10 w-full" }),
|
|
1519
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
|
|
1520
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-40 w-full rounded-xl" }),
|
|
1521
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-40 w-full rounded-xl" })
|
|
1522
|
-
] })
|
|
1523
|
-
] });
|
|
1524
|
-
}
|
|
1525
|
-
function statusVariant2(s) {
|
|
1526
|
-
if (s === "paid") return "success";
|
|
1527
|
-
if (s === "pending") return "secondary";
|
|
1528
|
-
if (s === "refunded" || s === "chargeback") return "destructive";
|
|
1529
|
-
return "outline";
|
|
1530
|
-
}
|
|
1531
|
-
var salesCache = null;
|
|
1532
|
-
function SalesPage() {
|
|
1533
|
-
const t = core.useTranslation();
|
|
1534
|
-
const [orders, setOrders] = React3__namespace.useState(salesCache);
|
|
1535
|
-
React3__namespace.useEffect(() => {
|
|
1536
|
-
if (salesCache) return;
|
|
1537
|
-
let alive = true;
|
|
1538
|
-
const p = courses.getCoursesProvider();
|
|
1539
|
-
void Promise.all([
|
|
1540
|
-
p.listCoreSales().catch(() => []),
|
|
1541
|
-
p.listOrders().catch(() => [])
|
|
1542
|
-
]).then(([core, store]) => {
|
|
1543
|
-
if (!alive) return;
|
|
1544
|
-
const byId = /* @__PURE__ */ new Map();
|
|
1545
|
-
for (const o of store) byId.set(o.id, o);
|
|
1546
|
-
for (const o of core) byId.set(o.id, o);
|
|
1547
|
-
const list = [...byId.values()].sort((a, b) => a.createdAt < b.createdAt ? 1 : -1);
|
|
1548
|
-
salesCache = list;
|
|
1549
|
-
setOrders(list);
|
|
1550
|
-
});
|
|
1551
|
-
return () => {
|
|
1552
|
-
alive = false;
|
|
1553
|
-
};
|
|
1554
|
-
}, []);
|
|
1555
|
-
const paid = (orders ?? []).filter((o) => o.financialStatus === "paid");
|
|
1556
|
-
const revenue = paid.reduce((s, o) => s + o.total, 0);
|
|
1557
|
-
const currency = orders?.[0]?.currency ?? "BRL";
|
|
1558
|
-
const columns = [
|
|
1559
|
-
{ key: "date", header: t("courses.sales.date") || "Data", render: (o) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatDate(o.createdAt) }) },
|
|
1560
|
-
{ key: "customer", header: t("courses.sales.customer") || "Cliente", render: (o) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
1561
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "font-medium text-foreground", children: o.customerName ?? "\u2014" }),
|
|
1562
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-muted-foreground", children: o.customerEmail })
|
|
1563
|
-
] }) },
|
|
1564
|
-
{ key: "method", header: t("courses.sales.method") || "Forma", render: (o) => /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "capitalize", children: [
|
|
1565
|
-
o.paymentMethod ?? "\u2014",
|
|
1566
|
-
o.installmentCount && o.installmentCount > 1 ? ` \xB7 ${o.installmentCount}x` : ""
|
|
1567
|
-
] }) },
|
|
1568
|
-
{ 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 }) },
|
|
1569
|
-
{ key: "fee", header: t("courses.sales.fee") || "Taxa", align: "right", render: (o) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatMoney(o.platformFee, o.currency) }) },
|
|
1570
|
-
{ key: "total", header: t("courses.sales.amount") || "Valor", align: "right", render: (o) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: formatMoney(o.total, o.currency) }) }
|
|
1571
|
-
];
|
|
1572
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1573
|
-
saas.OrderListView,
|
|
1574
|
-
{
|
|
1575
|
-
title: t("courses.sales.title") || "Vendas",
|
|
1576
|
-
subtitle: t("courses.sales.subtitle") || "Todas as transa\xE7\xF5es dos seus cursos.",
|
|
1577
|
-
stats: [
|
|
1578
|
-
{ label: t("courses.sales.count") || "Vendas", value: String(paid.length) },
|
|
1579
|
-
{ label: t("courses.sales.revenue") || "Receita", value: formatMoney(revenue, currency) }
|
|
1580
|
-
],
|
|
1581
|
-
columns,
|
|
1582
|
-
rows: orders ?? [],
|
|
1583
|
-
loading: orders === null,
|
|
1584
|
-
empty: t("courses.sales.empty") || "Nenhuma venda ainda.",
|
|
1585
|
-
onRowClick: (o) => navigateTo(`/courses/sales/${o.id}`)
|
|
1586
|
-
}
|
|
1587
|
-
);
|
|
1588
|
-
}
|
|
1589
|
-
SalesPage.displayName = "SalesPage";
|
|
1590
|
-
var Page = ({ children }) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mx-auto max-w-6xl p-6 md:p-8", children });
|
|
1591
|
-
var orderCache = /* @__PURE__ */ new Map();
|
|
1592
|
-
function statusTone(s) {
|
|
1593
|
-
if (s === "paid") return "success";
|
|
1594
|
-
if (s === "pending") return "warning";
|
|
1595
|
-
if (s === "refunded" || s === "chargeback") return "destructive";
|
|
1596
|
-
return "muted";
|
|
1597
|
-
}
|
|
1598
|
-
function instTone(s) {
|
|
1599
|
-
if (s === "paid") return "success";
|
|
1600
|
-
if (s === "overdue") return "destructive";
|
|
1601
|
-
return "warning";
|
|
1602
|
-
}
|
|
1603
|
-
function CourseOrderDetailView({ id }) {
|
|
1604
|
-
const t = core.useTranslation();
|
|
1605
|
-
const [order, setOrder] = React3__namespace.useState(() => id ? orderCache.get(id) ?? null : null);
|
|
1606
|
-
const [loading, setLoading] = React3__namespace.useState(() => !(id && orderCache.has(id)));
|
|
1607
|
-
React3__namespace.useEffect(() => {
|
|
1608
|
-
let alive = true;
|
|
1609
|
-
if (!id) {
|
|
1610
|
-
setLoading(false);
|
|
1611
|
-
return;
|
|
1612
|
-
}
|
|
1613
|
-
const cached = orderCache.get(id);
|
|
1614
|
-
if (cached) {
|
|
1615
|
-
setOrder(cached);
|
|
1616
|
-
setLoading(false);
|
|
1617
|
-
return;
|
|
1618
|
-
}
|
|
1619
|
-
setLoading(true);
|
|
1620
|
-
void courses.getCoursesProvider().getOrder(id).then((o) => {
|
|
1621
|
-
if (!alive) return;
|
|
1622
|
-
if (o) orderCache.set(id, o);
|
|
1623
|
-
setOrder(o);
|
|
1624
|
-
setLoading(false);
|
|
1625
|
-
});
|
|
1626
|
-
return () => {
|
|
1627
|
-
alive = false;
|
|
1628
|
-
};
|
|
1629
|
-
}, [id]);
|
|
1630
|
-
const back = () => navigateTo("/courses/sales");
|
|
1631
|
-
if (loading) return /* @__PURE__ */ jsxRuntime.jsx(Page, { children: /* @__PURE__ */ jsxRuntime.jsx(saas.OrderDetailSkeleton, {}) });
|
|
1632
|
-
if (!order) {
|
|
1633
|
-
return /* @__PURE__ */ jsxRuntime.jsx(Page, { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1634
|
-
EmptyState,
|
|
1635
|
-
{
|
|
1636
|
-
title: t("courses.order.notFound") || "Pedido n\xE3o encontrado",
|
|
1637
|
-
description: "",
|
|
1638
|
-
action: /* @__PURE__ */ jsxRuntime.jsx("button", { className: "text-sm text-primary underline", onClick: back, children: t("common.back") || "Voltar" })
|
|
1639
|
-
}
|
|
1640
|
-
) });
|
|
1641
|
-
}
|
|
1642
|
-
const cur = order.currency;
|
|
1643
|
-
const installments = order.installments.map((m) => ({
|
|
1644
|
-
number: m.installmentNumber,
|
|
1645
|
-
amount: formatMoney(m.amount, cur),
|
|
1646
|
-
status: m.status,
|
|
1647
|
-
statusTone: instTone(m.status),
|
|
1648
|
-
dueDate: m.dueDate ? formatDate(m.dueDate) : null,
|
|
1649
|
-
paymentDate: m.paymentDate ? formatDate(m.paymentDate) : null,
|
|
1650
|
-
note: m.cardBrand ? `${m.cardBrand}${m.cardInstallments ? ` ${m.cardInstallments}x` : ""}` : null
|
|
1651
|
-
}));
|
|
1652
|
-
const data = {
|
|
1653
|
-
title: `${t("courses.order.title") || "Pedido"} #${order.id.slice(0, 8)}`,
|
|
1654
|
-
subtitle: formatDate(order.createdAt),
|
|
1655
|
-
badges: [
|
|
1656
|
-
{ label: t(`courses.financialStatus.${order.financialStatus}`) || order.financialStatus, tone: statusTone(order.financialStatus) },
|
|
1657
|
-
...order.installmentCount && order.installmentCount > 1 ? [{ label: `${order.installmentCount}x`, tone: "muted" }] : []
|
|
1658
|
-
],
|
|
1659
|
-
customer: { name: order.customerName, email: order.customerEmail },
|
|
1660
|
-
customerNode: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1 text-sm", children: [
|
|
1661
|
-
order.customerId ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1662
|
-
saas.PersonLink,
|
|
1663
|
-
{
|
|
1664
|
-
personId: order.customerId,
|
|
1665
|
-
name: order.customerName ?? order.customerEmail ?? "Cliente",
|
|
1666
|
-
className: "font-medium"
|
|
1667
|
-
}
|
|
1668
|
-
) : /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-medium", children: order.customerName ?? "\u2014" }),
|
|
1669
|
-
order.customerEmail && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground", children: order.customerEmail })
|
|
1670
|
-
] }),
|
|
1671
|
-
items: [{
|
|
1672
|
-
name: order.courseTitle ?? "\u2014",
|
|
1673
|
-
description: order.offerName,
|
|
1674
|
-
quantity: 1,
|
|
1675
|
-
unitPrice: formatMoney(order.total, cur),
|
|
1676
|
-
total: formatMoney(order.total, cur)
|
|
1677
|
-
}],
|
|
1678
|
-
totals: [
|
|
1679
|
-
{ label: "Total", value: formatMoney(order.total, cur), strong: true },
|
|
1680
|
-
...order.platformFee > 0 ? [{ label: "Taxa", value: `\u2212 ${formatMoney(order.platformFee, cur)}`, muted: true }] : [],
|
|
1681
|
-
{ label: "L\xEDquido", value: formatMoney(order.netValue, cur) }
|
|
1682
|
-
],
|
|
1683
|
-
paymentMethod: order.paymentMethod ?? void 0,
|
|
1684
|
-
receivable: {
|
|
1685
|
-
rows: [
|
|
1686
|
-
{ label: "Recebido", value: formatMoney(order.paidAmount, cur) },
|
|
1687
|
-
{ label: "Saldo", value: formatMoney(order.balance, cur), strong: order.balance > 0 }
|
|
1688
|
-
],
|
|
1689
|
-
installments,
|
|
1690
|
-
emptyLabel: t("courses.order.noInstallments") || "Sem parcelas."
|
|
1691
|
-
},
|
|
1692
|
-
onBack: back,
|
|
1693
|
-
backLabel: t("courses.sales.title") || "Vendas"
|
|
1694
|
-
};
|
|
1695
|
-
return /* @__PURE__ */ jsxRuntime.jsx(Page, { children: /* @__PURE__ */ jsxRuntime.jsx(saas.OrderDetailView, { ...data }) });
|
|
1696
|
-
}
|
|
1697
|
-
CourseOrderDetailView.displayName = "CourseOrderDetailView";
|
|
1698
|
-
function statusVariant3(s) {
|
|
1699
|
-
if (s === "active") return "success";
|
|
1700
|
-
if (s === "past_due") return "secondary";
|
|
1701
|
-
return "destructive";
|
|
1702
|
-
}
|
|
1703
|
-
function SubscriptionsPage() {
|
|
1704
|
-
const t = core.useTranslation();
|
|
1705
|
-
const [subs, setSubs] = React3__namespace.useState(null);
|
|
1706
|
-
React3__namespace.useEffect(() => {
|
|
1707
|
-
void courses.getCoursesProvider().listSubscriptions().then(setSubs);
|
|
1708
|
-
}, []);
|
|
1709
|
-
const active = (subs ?? []).filter((s) => s.status === "active");
|
|
1710
|
-
const mrr = active.reduce((sum, s) => sum + (s.interval === "year" ? s.netValue / 12 : s.netValue), 0);
|
|
1711
|
-
const currency = subs?.[0]?.currency ?? "BRL";
|
|
1712
|
-
const columns = [
|
|
1713
|
-
{ key: "start", header: t("courses.subs.start") || "Start date", render: (s) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatDate(s.startedAt) }) },
|
|
1714
|
-
{ key: "customer", header: t("courses.subs.customer") || "Customer", render: (s) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
1715
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "font-medium text-foreground", children: s.customerName ?? "\u2014" }),
|
|
1716
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-muted-foreground", children: s.customerEmail })
|
|
1717
|
-
] }) },
|
|
1718
|
-
{ 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 }) },
|
|
1719
|
-
{ key: "value", header: t("courses.subs.value") || "Net value", align: "right", render: (s) => /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-medium text-foreground", children: [
|
|
1720
|
-
formatMoney(s.netValue, s.currency),
|
|
1721
|
-
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-muted-foreground", children: [
|
|
1722
|
-
" / ",
|
|
1723
|
-
t(`courses.interval.${s.interval}`) || s.interval
|
|
1724
|
-
] })
|
|
1725
|
-
] }) }
|
|
1726
|
-
];
|
|
1727
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(PageContainer, { title: t("courses.subs.title") || "Subscriptions", subtitle: t("courses.subs.subtitle") || "Recurring plans and MRR.", children: [
|
|
1728
|
-
/* @__PURE__ */ jsxRuntime.jsxs(StatGrid, { children: [
|
|
1729
|
-
/* @__PURE__ */ jsxRuntime.jsx(StatCard, { label: t("courses.subs.active") || "Active subscriptions", value: String(active.length) }),
|
|
1730
|
-
/* @__PURE__ */ jsxRuntime.jsx(StatCard, { label: t("courses.subs.mrr") || "Monthly recurring revenue", value: formatMoney(mrr, currency) })
|
|
1731
|
-
] }),
|
|
1732
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1733
|
-
SimpleTable,
|
|
1734
|
-
{
|
|
1735
|
-
columns,
|
|
1736
|
-
rows: subs ?? [],
|
|
1737
|
-
empty: t("courses.subs.empty") || "No subscriptions yet."
|
|
1738
|
-
}
|
|
1739
|
-
)
|
|
1740
|
-
] });
|
|
1741
|
-
}
|
|
1742
|
-
SubscriptionsPage.displayName = "SubscriptionsPage";
|
|
1743
|
-
function FinancialPage() {
|
|
1744
|
-
const t = core.useTranslation();
|
|
1745
|
-
const [summary, setSummary] = React3__namespace.useState(null);
|
|
1746
|
-
const [payouts, setPayouts] = React3__namespace.useState(null);
|
|
1747
|
-
const [account, setAccount] = React3__namespace.useState(null);
|
|
1748
|
-
React3__namespace.useEffect(() => {
|
|
1749
|
-
const p = courses.getCoursesProvider();
|
|
1750
|
-
void p.getFinancialSummary().then(setSummary);
|
|
1751
|
-
void p.listPayouts().then(setPayouts);
|
|
1752
|
-
void p.getCreatorAccount().then(setAccount);
|
|
1753
|
-
}, []);
|
|
1754
|
-
const currency = summary?.currency ?? "BRL";
|
|
1755
|
-
const connected = !!account?.stripeAccountId && account.chargesEnabled;
|
|
1756
|
-
function connectStripe() {
|
|
1757
|
-
ui.toast.info(t("courses.financial.connectSoon") || "Stripe Connect onboarding wires up in Stage 3.");
|
|
1758
|
-
}
|
|
1759
|
-
const columns = [
|
|
1760
|
-
{ key: "date", header: t("courses.financial.date") || "Date", render: (p) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: formatDate(p.createdAt) }) },
|
|
1761
|
-
{ 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) }) },
|
|
1762
|
-
{ 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 }) }
|
|
1763
|
-
];
|
|
1764
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1765
|
-
PageContainer,
|
|
1766
|
-
{
|
|
1767
|
-
title: t("courses.financial.title") || "Financial settings",
|
|
1768
|
-
subtitle: t("courses.financial.subtitle") || "Balance, payouts and your Stripe connection.",
|
|
1769
|
-
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" }),
|
|
1770
|
-
children: [
|
|
1771
|
-
!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: [
|
|
1772
|
-
t("courses.financial.notConnected") || "Connect your Stripe account to receive payouts. The platform fee is applied automatically on each sale.",
|
|
1773
|
-
account && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ml-1 text-muted-foreground", children: [
|
|
1774
|
-
"(",
|
|
1775
|
-
t("courses.financial.platformFee") || "Platform fee",
|
|
1776
|
-
": ",
|
|
1777
|
-
(account.platformFeeBps / 100).toFixed(2),
|
|
1778
|
-
"%)"
|
|
1779
|
-
] })
|
|
1780
|
-
] }),
|
|
1781
|
-
/* @__PURE__ */ jsxRuntime.jsxs(StatGrid, { children: [
|
|
1782
|
-
/* @__PURE__ */ jsxRuntime.jsx(StatCard, { label: t("courses.financial.available") || "Available balance", value: formatMoney(summary?.availableBalance ?? 0, currency) }),
|
|
1783
|
-
/* @__PURE__ */ jsxRuntime.jsx(StatCard, { label: t("courses.financial.pending") || "Pending balance", value: formatMoney(summary?.pendingBalance ?? 0, currency) }),
|
|
1784
|
-
/* @__PURE__ */ jsxRuntime.jsx(StatCard, { label: t("courses.financial.revenue") || "Total revenue", value: formatMoney(summary?.totalRevenue ?? 0, currency) }),
|
|
1785
|
-
/* @__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 })
|
|
1786
|
-
] }),
|
|
1787
|
-
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "mb-3 text-sm font-semibold text-foreground", children: t("courses.financial.payouts") || "Payouts" }),
|
|
1788
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1789
|
-
SimpleTable,
|
|
1790
|
-
{
|
|
1791
|
-
columns,
|
|
1792
|
-
rows: payouts ?? [],
|
|
1793
|
-
empty: t("courses.financial.noPayouts") || "No payouts yet."
|
|
1794
|
-
}
|
|
1795
|
-
)
|
|
1796
|
-
]
|
|
1797
|
-
}
|
|
1798
|
-
);
|
|
1799
|
-
}
|
|
1800
|
-
FinancialPage.displayName = "FinancialPage";
|
|
1801
|
-
function ReportsPage() {
|
|
1802
|
-
const t = core.useTranslation();
|
|
1803
|
-
const [rows, setRows] = React3__namespace.useState(null);
|
|
1804
|
-
React3__namespace.useEffect(() => {
|
|
1805
|
-
const p = courses.getCoursesProvider();
|
|
1806
|
-
void Promise.all([p.listCourses(), p.listOrders({ financialStatus: "paid" })]).then(([courses, orders]) => {
|
|
1807
|
-
setRows(buildRevenueByCourse(courses, orders));
|
|
1808
|
-
});
|
|
1809
|
-
}, []);
|
|
1810
|
-
const columns = [
|
|
1811
|
-
{ key: "title", header: t("courses.reports.course") || "Course", render: (r) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: r.title }) },
|
|
1812
|
-
{ key: "sales", header: t("courses.reports.sales") || "Sales", align: "right", render: (r) => /* @__PURE__ */ jsxRuntime.jsx("span", { children: r.sales }) },
|
|
1813
|
-
{ 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) }) }
|
|
1814
|
-
];
|
|
1815
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(PageContainer, { title: t("courses.reports.title") || "Reports", subtitle: t("courses.reports.subtitle") || "Revenue by course.", children: [
|
|
1816
|
-
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "mb-3 text-sm font-semibold text-foreground", children: t("courses.reports.byCourse") || "Revenue by course" }),
|
|
1817
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1818
|
-
SimpleTable,
|
|
1819
|
-
{
|
|
1820
|
-
columns,
|
|
1821
|
-
rows: rows ?? [],
|
|
1822
|
-
empty: t("courses.reports.empty") || "No revenue yet."
|
|
1823
|
-
}
|
|
1824
|
-
)
|
|
1825
|
-
] });
|
|
1826
|
-
}
|
|
1827
|
-
ReportsPage.displayName = "ReportsPage";
|
|
1828
|
-
function buildRevenueByCourse(courses, orders) {
|
|
1829
|
-
const byCourse = /* @__PURE__ */ new Map();
|
|
1830
|
-
for (const o of orders) {
|
|
1831
|
-
const agg = byCourse.get(o.courseId) ?? { sales: 0, revenue: 0, currency: o.currency };
|
|
1832
|
-
agg.sales += 1;
|
|
1833
|
-
agg.revenue = Math.round((agg.revenue + o.total) * 100) / 100;
|
|
1834
|
-
byCourse.set(o.courseId, agg);
|
|
1835
|
-
}
|
|
1836
|
-
return courses.map((c) => {
|
|
1837
|
-
const agg = byCourse.get(c.id);
|
|
1838
|
-
return { courseId: c.id, title: c.title, sales: agg?.sales ?? 0, revenue: agg?.revenue ?? 0, currency: agg?.currency ?? c.currency };
|
|
1839
|
-
}).sort((a, b) => b.revenue - a.revenue);
|
|
1840
|
-
}
|
|
1841
|
-
function CoursesGeneralSettings() {
|
|
1842
|
-
const t = core.useTranslation();
|
|
1843
|
-
const [currency, setCurrency] = React3__namespace.useState("BRL");
|
|
1844
|
-
const [methods, setMethods] = React3__namespace.useState({ card: true, pix: true, boleto: true });
|
|
1845
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-6", children: [
|
|
1846
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1847
|
-
saas.SettingsGroup,
|
|
1848
|
-
{
|
|
1849
|
-
title: t("courses.settings.payments") || "Payments",
|
|
1850
|
-
description: t("courses.settings.paymentsHelp") || "Connect Stripe in the Integrations tab; the platform fee applies automatically.",
|
|
1851
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1852
|
-
saas.SelectRow,
|
|
1853
|
-
{
|
|
1854
|
-
label: t("courses.settings.currency") || "Default currency",
|
|
1855
|
-
value: currency,
|
|
1856
|
-
onChange: setCurrency,
|
|
1857
|
-
options: [
|
|
1858
|
-
{ value: "BRL", label: "BRL (R$)" },
|
|
1859
|
-
{ value: "USD", label: "USD ($)" },
|
|
1860
|
-
{ value: "EUR", label: "EUR (\u20AC)" }
|
|
1861
|
-
]
|
|
1862
|
-
}
|
|
1863
|
-
)
|
|
1864
|
-
}
|
|
1865
|
-
),
|
|
1866
|
-
/* @__PURE__ */ jsxRuntime.jsxs(saas.SettingsGroup, { title: t("courses.settings.methods") || "Payment methods", children: [
|
|
1867
|
-
/* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t("courses.editor.payCard") || "Credit card", checked: methods.card, onChange: (v) => setMethods((m) => ({ ...m, card: v })) }),
|
|
1868
|
-
/* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t("courses.editor.payPix") || "Pix", checked: methods.pix, onChange: (v) => setMethods((m) => ({ ...m, pix: v })) }),
|
|
1869
|
-
/* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t("courses.editor.payBoleto") || "Boleto", checked: methods.boleto, onChange: (v) => setMethods((m) => ({ ...m, boleto: v })) })
|
|
1870
|
-
] })
|
|
1871
|
-
] });
|
|
1872
|
-
}
|
|
1873
|
-
CoursesGeneralSettings.displayName = "CoursesGeneralSettings";
|
|
1874
|
-
function CoursesSettingsTab() {
|
|
1875
|
-
const t = core.useTranslation();
|
|
1876
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1877
|
-
saas.PluginSettingsPanel,
|
|
1878
|
-
{
|
|
1879
|
-
title: t("courses.settings.title") || "Course settings",
|
|
1880
|
-
subtitle: t("courses.settings.subtitle") || "Payments, currency and checkout methods",
|
|
1881
|
-
generalSettings: /* @__PURE__ */ jsxRuntime.jsx(CoursesGeneralSettings, {}),
|
|
1882
|
-
hostPluginId: "courses",
|
|
1883
|
-
routeBase: "/settings/courses"
|
|
1884
|
-
}
|
|
1885
|
-
);
|
|
1886
|
-
}
|
|
1887
|
-
CoursesSettingsTab.displayName = "CoursesSettingsTab";
|
|
1888
|
-
function useSummary() {
|
|
1889
|
-
const [summary, setSummary] = React3__namespace.useState(null);
|
|
1890
|
-
React3__namespace.useEffect(() => {
|
|
1891
|
-
void courses.getCoursesProvider().getFinancialSummary().then(setSummary);
|
|
1892
|
-
}, []);
|
|
1893
|
-
return summary;
|
|
1894
|
-
}
|
|
1895
|
-
function RevenueKpi() {
|
|
1896
|
-
const t = core.useTranslation();
|
|
1897
|
-
const s = useSummary();
|
|
1898
|
-
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 });
|
|
1899
|
-
}
|
|
1900
|
-
function SalesKpi() {
|
|
1901
|
-
const t = core.useTranslation();
|
|
1902
|
-
const s = useSummary();
|
|
1903
|
-
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 });
|
|
1904
|
-
}
|
|
1905
|
-
function MrrKpi() {
|
|
1906
|
-
const t = core.useTranslation();
|
|
1907
|
-
const s = useSummary();
|
|
1908
|
-
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 });
|
|
1909
|
-
}
|
|
1910
|
-
function FeesKpi() {
|
|
1911
|
-
const t = core.useTranslation();
|
|
1912
|
-
const s = useSummary();
|
|
1913
|
-
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 });
|
|
1914
|
-
}
|
|
1915
|
-
function createCoursesDashboardWidgets(modules = {}) {
|
|
1916
|
-
return [
|
|
1917
|
-
...modules.sales ? [
|
|
1918
|
-
ui.defineKpiWidget({ id: "courses.kpi.revenue", title: "courses.kpi.revenue", domain: "courses", defaultOrder: 0, component: RevenueKpi }),
|
|
1919
|
-
ui.defineKpiWidget({ id: "courses.kpi.sales", title: "courses.kpi.sales", domain: "courses", defaultOrder: 1, component: SalesKpi })
|
|
1920
|
-
] : [],
|
|
1921
|
-
...modules.subscriptions ? [ui.defineKpiWidget({ id: "courses.kpi.mrr", title: "courses.kpi.mrr", domain: "courses", defaultOrder: 2, component: MrrKpi })] : [],
|
|
1922
|
-
...modules.financial ? [ui.defineKpiWidget({ id: "courses.kpi.fees", title: "courses.kpi.fees", domain: "courses", defaultOrder: 3, component: FeesKpi })] : []
|
|
1923
|
-
];
|
|
1924
|
-
}
|
|
1925
|
-
function createStripeConnector() {
|
|
1926
|
-
return {
|
|
1927
|
-
id: "stripe-connect",
|
|
1928
|
-
hostPluginId: "courses",
|
|
1929
|
-
name: "Stripe",
|
|
1930
|
-
description: "Receba os pagamentos dos alunos; a taxa da plataforma \xE9 aplicada automaticamente.",
|
|
1931
|
-
icon: "CreditCard",
|
|
1932
|
-
authKind: "oauth",
|
|
1933
|
-
async getStatus() {
|
|
1934
|
-
try {
|
|
1935
|
-
const account = await courses.getCoursesProvider().getCreatorAccount();
|
|
1936
|
-
const fee = (account.platformFeeBps / 100).toFixed(2);
|
|
1937
|
-
return {
|
|
1938
|
-
connected: !!account.stripeAccountId && account.chargesEnabled,
|
|
1939
|
-
detail: `Taxa da plataforma: ${fee}%`
|
|
1940
|
-
};
|
|
1941
|
-
} catch {
|
|
1942
|
-
return { connected: false };
|
|
1943
|
-
}
|
|
1944
|
-
},
|
|
1945
|
-
async startOAuth(_redirectTo) {
|
|
1946
|
-
throw new Error("O onboarding do Stripe Connect \xE9 ligado na Etapa 3.");
|
|
1947
|
-
}
|
|
1948
|
-
};
|
|
1949
|
-
}
|
|
1950
|
-
|
|
1951
|
-
// src/index.ts
|
|
1952
|
-
var asComp = (c) => c;
|
|
1953
|
-
var read = (feature) => ({ feature, action: "read" });
|
|
1954
|
-
function createCoursesPlugin(options) {
|
|
1955
|
-
core.registerTranslations(coursesLocales);
|
|
1956
|
-
courses.setCoursesTenantResolver(() => saas.useOrganizationStore.getState().currentOrg?.id);
|
|
1957
|
-
if (!courses.getCoursesProviderOptional()) {
|
|
1958
|
-
courses.setCoursesProvider(courses.createMockCoursesProvider());
|
|
1959
|
-
}
|
|
1960
|
-
const base = options?.navPosition ?? 1;
|
|
1961
|
-
const section = options?.navSection ?? "main";
|
|
1962
|
-
const modules = options?.modules ?? {};
|
|
1963
|
-
const labels = options?.labels ?? {};
|
|
1964
|
-
const navigation = [
|
|
1965
|
-
{ section, position: base, label: options?.navLabel ?? "Courses", route: "/courses", icon: "BookOpen", permission: read("courses") },
|
|
1966
|
-
// The members area is no longer a standalone nav entry — it lives inside each
|
|
1967
|
-
// course's record (CourseEditorPage → "Alunos" tab). modules.membersArea is
|
|
1968
|
-
// kept accepted-but-inert for backward compat with existing manifests.
|
|
1969
|
-
...modules.sales ? [{ section, position: base + 2, label: labels.sales ?? "Sales", route: "/courses/sales", icon: "TrendingUp", permission: read("courses.sales") }] : [],
|
|
1970
|
-
...modules.subscriptions ? [{ section, position: base + 3, label: labels.subscriptions ?? "Subscriptions", route: "/courses/subscriptions", icon: "Repeat", permission: read("courses.sales") }] : [],
|
|
1971
|
-
...modules.financial ? [{ section, position: base + 4, label: labels.financial ?? "Financial", route: "/courses/financial", icon: "CreditCard", permission: read("courses.finance") }] : [],
|
|
1972
|
-
...modules.reports ? [{ section, position: base + 5, label: labels.reports ?? "Reports", route: "/courses/reports", icon: "BarChart3", permission: read("courses.sales") }] : []
|
|
1973
|
-
];
|
|
1974
|
-
const routes = [
|
|
1975
|
-
// Static segments outscore the `:id` param in the saas router, so these
|
|
1976
|
-
// resolve before the editor route (AdminShell sorts by routeScore).
|
|
1977
|
-
{ path: "/courses", component: asComp(CoursesListPage), permission: read("courses") },
|
|
1978
|
-
...modules.sales ? [{ path: "/courses/sales", component: asComp(SalesPage), permission: read("courses.sales") }] : [],
|
|
1979
|
-
// Order detail — static `/courses/sales/:id` outscores `/courses/:id`.
|
|
1980
|
-
...modules.sales ? [{ path: "/courses/sales/:id", component: asComp(CourseOrderDetailView), permission: read("courses.sales") }] : [],
|
|
1981
|
-
...modules.subscriptions ? [{ path: "/courses/subscriptions", component: asComp(SubscriptionsPage), permission: read("courses.sales") }] : [],
|
|
1982
|
-
...modules.financial ? [{ path: "/courses/financial", component: asComp(FinancialPage), permission: read("courses.finance") }] : [],
|
|
1983
|
-
...modules.reports ? [{ path: "/courses/reports", component: asComp(ReportsPage), permission: read("courses.sales") }] : [],
|
|
1984
|
-
{ path: "/courses/:id", component: asComp(CourseEditorPage) }
|
|
1985
|
-
];
|
|
1986
|
-
return {
|
|
1987
|
-
id: "courses",
|
|
1988
|
-
name: options?.navLabel ?? "Courses",
|
|
1989
|
-
icon: "BookOpen",
|
|
1990
|
-
version: "1.0.0",
|
|
1991
|
-
scope: options?.scope ?? "vertical",
|
|
1992
|
-
verticalId: options?.verticalId,
|
|
1993
|
-
// LMS is its own business model with opt-in commerce modules
|
|
1994
|
-
// (members/sales/subscriptions — see modules.* below) — targets the
|
|
1995
|
-
// saas/admin Panel world rather than the ecommerce storefront.
|
|
1996
|
-
scaffolds: ["saas"],
|
|
1997
|
-
defaultEnabled: true,
|
|
1998
|
-
dependencies: [],
|
|
1999
|
-
declaredFeatures: [
|
|
2000
|
-
{ id: "courses", label: "Courses", group: "Courses" },
|
|
2001
|
-
{ id: "courses.sales", label: "Sales", group: "Courses" },
|
|
2002
|
-
{ id: "courses.finance", label: "Financial", group: "Courses" }
|
|
2003
|
-
],
|
|
2004
|
-
declaredLimits: [
|
|
2005
|
-
{ key: "courses", label: "Courses", table: "plg_courses_courses" }
|
|
2006
|
-
],
|
|
2007
|
-
navigation,
|
|
2008
|
-
routes,
|
|
2009
|
-
widgets: [],
|
|
2010
|
-
// Commerce KPIs on the shared 'home' surface follow the same opt-in rule:
|
|
2011
|
-
// no module enabled → no widgets broadcast into a host's dashboard.
|
|
2012
|
-
dashboardWidgets: createCoursesDashboardWidgets(modules),
|
|
2013
|
-
connectors: [createStripeConnector()],
|
|
2014
|
-
settings: [
|
|
2015
|
-
{ id: "courses", label: "Courses", icon: "BookOpen", component: asComp(CoursesSettingsTab), order: 20, permission: read("courses") }
|
|
2016
|
-
],
|
|
2017
|
-
locales: coursesLocales
|
|
2018
|
-
};
|
|
2019
|
-
}
|
|
2020
|
-
core.registerPluginFactory("courses", createCoursesPlugin);
|
|
2021
|
-
|
|
2022
|
-
exports.coursesLocales = coursesLocales;
|
|
2023
|
-
exports.createCoursesPlugin = createCoursesPlugin;
|
|
2024
|
-
//# sourceMappingURL=index.cjs.map
|
|
2025
|
-
//# sourceMappingURL=index.cjs.map
|