@fayz-ai/plugin-courses 0.8.1 → 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/components/CommerceUI.d.ts +31 -1
  2. package/dist/components/CommerceUI.d.ts.map +1 -1
  3. package/dist/index.cjs +1237 -301
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +1240 -304
  7. package/dist/index.js.map +1 -1
  8. package/dist/locales/en.d.ts.map +1 -1
  9. package/dist/locales/pt-BR.d.ts.map +1 -1
  10. package/dist/pages/CourseEditorPage.d.ts.map +1 -1
  11. package/dist/pages/CoursesListPage.d.ts.map +1 -1
  12. package/dist/pages/SalesPage.d.ts.map +1 -1
  13. package/dist/public/CheckoutScreen.d.ts +17 -0
  14. package/dist/public/CheckoutScreen.d.ts.map +1 -0
  15. package/dist/public/createPublicCheckoutPlugin.d.ts +27 -0
  16. package/dist/public/createPublicCheckoutPlugin.d.ts.map +1 -0
  17. package/dist/public/data.d.ts +28 -0
  18. package/dist/public/data.d.ts.map +1 -0
  19. package/dist/public/data.supabase.d.ts +7 -0
  20. package/dist/public/data.supabase.d.ts.map +1 -0
  21. package/dist/public/index.d.ts +9 -0
  22. package/dist/public/index.d.ts.map +1 -0
  23. package/dist/views/CourseOrderDetailView.d.ts +10 -0
  24. package/dist/views/CourseOrderDetailView.d.ts.map +1 -0
  25. package/package.json +14 -5
  26. package/src/components/CommerceUI.tsx +78 -2
  27. package/src/index.ts +6 -3
  28. package/src/locales/en.ts +73 -12
  29. package/src/locales/pt-BR.ts +73 -12
  30. package/src/pages/CourseEditorPage.tsx +990 -177
  31. package/src/pages/CoursesListPage.tsx +20 -1
  32. package/src/pages/SalesPage.tsx +49 -20
  33. package/src/public/CheckoutScreen.tsx +279 -0
  34. package/src/public/createPublicCheckoutPlugin.tsx +88 -0
  35. package/src/public/data.supabase.ts +63 -0
  36. package/src/public/data.ts +89 -0
  37. package/src/public/index.ts +26 -0
  38. package/src/views/CourseOrderDetailView.tsx +132 -0
  39. package/dist/pages/MembersAreaPage.d.ts +0 -6
  40. package/dist/pages/MembersAreaPage.d.ts.map +0 -1
  41. package/src/pages/MembersAreaPage.tsx +0 -60
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 React2 from 'react';
5
- import { defineKpiWidget, useModuleLayout, PageHeaderActions, Button, Badge, Input, KpiCard, DataTable, Card, toast } from '@fayz-ai/ui';
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
- "courses.editor.tab.settings": "Payment",
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 a customer can buy. Mark one default; order-bumps show on other checkouts.",
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": "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
- "courses.editor.tab.settings": "Pagamento",
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 que o cliente pode comprar. Marque um padr\xE3o; order-bumps aparecem nos checkouts das outras.",
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": "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] = React2.useState(null);
316
- const [creating, setCreating] = React2.useState(false);
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 = React2.useCallback(async () => {
445
+ const load = React3.useCallback(async () => {
320
446
  setCourses(await getCoursesProvider().listCourses());
321
447
  }, []);
322
- React2.useEffect(() => {
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__ */ jsx("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3", children: courses?.map((course) => /* @__PURE__ */ jsxs(
350
- "button",
351
- {
352
- onClick: () => navigateTo(`/courses/${course.id}`),
353
- className: "group overflow-hidden rounded-xl border border-border bg-card text-left transition hover:border-primary hover:shadow-md",
354
- children: [
355
- /* @__PURE__ */ jsx("div", { className: "aspect-video w-full overflow-hidden bg-muted", children: course.thumbnailUrl && // eslint-disable-next-line @next/next/no-img-element
356
- /* @__PURE__ */ jsx("img", { src: course.thumbnailUrl, alt: "", className: "h-full w-full object-cover" }) }),
357
- /* @__PURE__ */ jsxs("div", { className: "space-y-2 p-4", children: [
358
- /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-2", children: [
359
- /* @__PURE__ */ jsx("h3", { className: "font-semibold text-foreground line-clamp-2", children: course.title }),
360
- /* @__PURE__ */ jsx(Badge, { variant: statusVariant(course.status), children: t(`courses.status.${course.status}`) || course.status })
361
- ] }),
362
- course.subtitle && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground line-clamp-2", children: course.subtitle }),
363
- /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-foreground", children: course.price > 0 ? `${course.currency} ${course.price.toFixed(2)}` : "\u2014" })
364
- ] })
365
- ]
366
- },
367
- course.id
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 TABS = ["details", "curriculum", "offers", "members", "settings", "checkout", "links"];
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] = React2.useState("details");
396
- const [course, setCourse] = React2.useState(null);
397
- const [modules, setModules] = React2.useState([]);
398
- const [lessons, setLessons] = React2.useState([]);
399
- const [offers, setOffers] = React2.useState([]);
400
- const reload = React2.useCallback(async () => {
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
- const [m, l, o] = await Promise.all([
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
- React2.useEffect(() => {
672
+ React3.useEffect(() => {
417
673
  void reload();
418
674
  }, [reload]);
419
- if (!course) {
420
- return /* @__PURE__ */ jsx("div", { className: "p-8 text-muted-foreground", children: "\u2026" });
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
- await getCoursesProvider().updateCourse(course.id, {
424
- title: course.title,
425
- subtitle: course.subtitle ?? void 0,
426
- description: course.description ?? void 0,
427
- price: course.price,
428
- status: course.status
429
- });
430
- toast.success(t("courses.editor.saved") || "Course saved");
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 patchCourse(patch) {
433
- setCourse((c) => c ? { ...c, ...patch } : c);
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
- await getCoursesProvider().createLesson({
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
- videoUrl: "https://www.youtube.com/embed/dQw4w9WgXcQ"
452
- });
453
- await reload();
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 deleteLesson(lessonId) {
464
- await getCoursesProvider().deleteLesson(lessonId);
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("button", { onClick: () => navigateTo("/courses"), className: "mb-4 text-sm text-muted-foreground hover:text-foreground", children: [
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: () => setTab(tk),
501
- className: `-mb-px whitespace-nowrap border-b-2 px-3 py-2 text-sm font-medium ${tab === tk ? "border-primary text-foreground" : "border-transparent text-muted-foreground hover:text-foreground"}`,
502
- children: t(`courses.editor.tab.${tk}`) || tk
503
- },
504
- tk
505
- )) }),
506
- tab === "details" && /* @__PURE__ */ jsxs("div", { className: "max-w-xl space-y-4", children: [
507
- /* @__PURE__ */ jsx(Field, { label: t("courses.editor.fieldTitle") || "Title", children: /* @__PURE__ */ jsx(Input, { value: course.title, onChange: (e) => patchCourse({ title: e.target.value }) }) }),
508
- /* @__PURE__ */ jsx(Field, { label: t("courses.editor.fieldSubtitle") || "Subtitle", children: /* @__PURE__ */ jsx(Input, { value: course.subtitle ?? "", onChange: (e) => patchCourse({ subtitle: e.target.value }) }) }),
509
- /* @__PURE__ */ jsx(Field, { label: t("courses.editor.fieldDescription") || "Description", children: /* @__PURE__ */ jsx(
510
- "textarea",
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
- className: "min-h-24 w-full rounded-md border border-input bg-background px-3 py-2 text-sm",
513
- value: course.description ?? "",
514
- onChange: (e) => patchCourse({ description: e.target.value })
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("div", { className: "flex gap-4", children: [
518
- /* @__PURE__ */ jsx(Field, { label: t("courses.editor.fieldPrice") || "Price", children: /* @__PURE__ */ jsx(Input, { type: "number", value: String(course.price), onChange: (e) => patchCourse({ price: Number(e.target.value) || 0 }) }) }),
519
- /* @__PURE__ */ jsx(Field, { label: t("courses.editor.fieldStatus") || "Status", children: /* @__PURE__ */ jsx(
520
- "select",
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
- className: "h-10 rounded-md border border-input bg-background px-3 text-sm",
523
- value: course.status,
524
- onChange: (e) => patchCourse({ status: e.target.value }),
525
- children: STATUSES.map((s) => /* @__PURE__ */ jsx("option", { value: s, children: t(`courses.status.${s}`) || s }, s))
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__ */ jsx(Button, { onClick: saveDetails, children: t("courses.editor.save") || "Save" })
530
- ] }),
531
- tab === "curriculum" && /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
532
- modules.map((mod) => /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-border bg-card p-4", children: [
533
- /* @__PURE__ */ jsxs("div", { className: "mb-3 flex items-center gap-2", children: [
534
- /* @__PURE__ */ jsx(
535
- Input,
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: "font-medium",
538
- defaultValue: mod.title,
539
- onBlur: (e) => renameModule(mod.id, e.target.value),
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
- /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: () => deleteModule(mod.id), children: t("courses.editor.deleteModule") || "Delete module" })
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
- lessons.filter((l) => l.moduleId === mod.id).map((lesson) => /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 gap-2 rounded-md border border-border p-3 md:grid-cols-[1fr_1fr_90px_auto]", children: [
547
- /* @__PURE__ */ jsx(Input, { defaultValue: lesson.title, placeholder: t("courses.editor.lessonTitle") || "Lesson title", onBlur: (e) => patchLesson(lesson.id, { title: e.target.value }) }),
548
- /* @__PURE__ */ jsx(Input, { defaultValue: lesson.videoUrl, placeholder: t("courses.editor.lessonVideo") || "Video URL (embed)", onBlur: (e) => patchLesson(lesson.id, { videoUrl: e.target.value }) }),
549
- /* @__PURE__ */ jsx(Input, { type: "number", defaultValue: String(Math.round(lesson.durationSec / 60)), onBlur: (e) => patchLesson(lesson.id, { durationSec: (Number(e.target.value) || 0) * 60 }), "aria-label": t("courses.editor.lessonDuration") || "Duration (min)" }),
550
- /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: () => deleteLesson(lesson.id), children: t("courses.editor.deleteLesson") || "Remove" })
551
- ] }, lesson.id)),
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
- tab === "members" && /* @__PURE__ */ jsxs("div", { className: "space-y-3 text-sm", children: [
589
- /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: t("courses.editor.membersHelp") || "Buyers of this course are added to its members area automatically. Access groups per offer:" }),
590
- /* @__PURE__ */ jsx("div", { className: "rounded-xl border border-border bg-card divide-y divide-border", children: offers.map((offer) => /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-4 py-3", children: [
591
- /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: offer.name }),
592
- /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: formatMoney(offer.price, offer.currency) }),
593
- /* @__PURE__ */ jsx(Badge, { variant: "secondary", children: t("courses.editor.defaultGroup") || "Default group" })
594
- ] }, offer.id)) })
595
- ] }),
596
- tab === "settings" && /* @__PURE__ */ jsxs("div", { className: "max-w-xl space-y-4 text-sm", children: [
597
- /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: t("courses.editor.settingsHelp") || "Payment methods offered at checkout (Stripe)." }),
598
- [
599
- ["card", t("courses.editor.payCard") || "Credit card (up to 12x)"],
600
- ["pix", t("courses.editor.payPix") || "Pix"],
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 Field({ label, children }) {
625
- return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
626
- /* @__PURE__ */ jsx("label", { className: "text-sm font-medium text-foreground", children: label }),
627
- children
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 PageContainer({ title, subtitle, actions, children }) {
631
- return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-6xl p-6 md:p-8", children: [
632
- /* @__PURE__ */ jsxs("div", { className: "mb-6 flex items-start justify-between gap-4", children: [
633
- /* @__PURE__ */ jsxs("div", { children: [
634
- /* @__PURE__ */ jsx("h1", { className: "text-2xl font-bold text-foreground", children: title }),
635
- subtitle && /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: subtitle })
636
- ] }),
637
- actions
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 StatCard({ label, value, hint }) {
643
- return /* @__PURE__ */ jsxs(Card, { className: "p-5", children: [
644
- /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: label }),
645
- /* @__PURE__ */ jsx("p", { className: "mt-1 text-2xl font-bold text-foreground", children: value }),
646
- hint && /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: hint })
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 StatGrid({ children }) {
650
- return /* @__PURE__ */ jsx("div", { className: "mb-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4", children });
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 SimpleTable({ columns, rows, empty, loading }) {
653
- const cols = React2.useMemo(
654
- () => columns.map((c) => ({
655
- id: c.key,
656
- header: () => /* @__PURE__ */ jsx("div", { className: c.align === "right" ? "text-right" : "", children: c.header }),
657
- cell: ({ row }) => /* @__PURE__ */ jsx("div", { className: c.align === "right" ? "text-right" : "", children: c.render(row.original) })
658
- })),
659
- [columns]
660
- );
661
- return /* @__PURE__ */ jsx(DataTable, { columns: cols, data: rows, emptyMessage: empty, loading });
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] = React2.useState(null);
672
- React2.useEffect(() => {
673
- void getCoursesProvider().listOrders().then(setOrders);
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") || "Date", render: (o) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: formatDate(o.createdAt) }) },
680
- { key: "customer", header: t("courses.sales.customer") || "Customer", render: (o) => /* @__PURE__ */ jsxs("div", { children: [
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") || "Method", render: (o) => /* @__PURE__ */ jsx("span", { className: "capitalize", children: o.paymentMethod ?? "\u2014" }) },
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") || "Platform fee", align: "right", render: (o) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: formatMoney(o.platformFee, o.currency) }) },
687
- { key: "total", header: t("courses.sales.amount") || "Amount", align: "right", render: (o) => /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: formatMoney(o.total, o.currency) }) }
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__ */ jsxs(PageContainer, { title: t("courses.sales.title") || "Sales", subtitle: t("courses.sales.subtitle") || "Every transaction across your courses.", children: [
690
- /* @__PURE__ */ jsxs(StatGrid, { children: [
691
- /* @__PURE__ */ jsx(StatCard, { label: t("courses.sales.count") || "Sales", value: String(paid.length) }),
692
- /* @__PURE__ */ jsx(StatCard, { label: t("courses.sales.revenue") || "Revenue", value: formatMoney(revenue, currency) })
693
- ] }),
694
- /* @__PURE__ */ jsx(
695
- SimpleTable,
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
- columns,
698
- rows: orders ?? [],
699
- empty: t("courses.sales.empty") || "No sales yet."
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
- SalesPage.displayName = "SalesPage";
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] = React2.useState(null);
713
- React2.useEffect(() => {
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] = React2.useState(null);
753
- const [payouts, setPayouts] = React2.useState(null);
754
- const [account, setAccount] = React2.useState(null);
755
- React2.useEffect(() => {
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] = React2.useState(null);
811
- React2.useEffect(() => {
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] = React2.useState("BRL");
889
- const [methods, setMethods] = React2.useState({ card: true, pix: true, boleto: true });
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] = React2.useState(null);
935
- React2.useEffect(() => {
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
- ...modules.membersArea ? [{ section, position: base + 1, label: labels.membersArea ?? "Members area", route: "/courses/members", icon: "GraduationCap", permission: read("courses") }] : [],
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") }] : [],