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