@fayz-ai/plugin-courses 0.10.49 → 0.10.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CourseEditorPage-GEBTVW7G.js +939 -0
- package/dist/CourseEditorPage-GEBTVW7G.js.map +1 -0
- package/dist/CourseOrderDetailView-LWHDQNZ7.js +122 -0
- package/dist/CourseOrderDetailView-LWHDQNZ7.js.map +1 -0
- package/dist/CoursesListPage-WDG3YHF4.js +95 -0
- package/dist/CoursesListPage-WDG3YHF4.js.map +1 -0
- package/dist/CoursesSettings-RLLYRP2R.js +56 -0
- package/dist/CoursesSettings-RLLYRP2R.js.map +1 -0
- package/dist/FinancialPage-UXD25JYA.js +53 -0
- package/dist/FinancialPage-UXD25JYA.js.map +1 -0
- package/dist/ReportsPage-ZC3QUISB.js +50 -0
- package/dist/ReportsPage-ZC3QUISB.js.map +1 -0
- package/dist/SalesPage-VNYJXPTU.js +85 -0
- package/dist/SalesPage-VNYJXPTU.js.map +1 -0
- package/dist/SubscriptionsPage-IGV723DM.js +66 -0
- package/dist/SubscriptionsPage-IGV723DM.js.map +1 -0
- package/dist/chunk-4DMJIPDN.js +11 -0
- package/dist/chunk-4DMJIPDN.js.map +1 -0
- package/dist/chunk-A4BEN6VV.js +15 -0
- package/dist/chunk-A4BEN6VV.js.map +1 -0
- package/dist/chunk-U5LQDCGG.js +92 -0
- package/dist/chunk-U5LQDCGG.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -1478
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
|
@@ -0,0 +1,939 @@
|
|
|
1
|
+
import { navigateTo } from './chunk-4DMJIPDN.js';
|
|
2
|
+
import { SectionCard, InfoNote, EmptyState } from './chunk-U5LQDCGG.js';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { useSaveBar, PageHeader, toast, Badge, Separator, Tabs, TabsList, TabsTrigger, TabsContent, Input, Textarea, SegmentedControl, Button, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, Checkbox, ConfirmDialog, Skeleton } from '@fayz-ai/ui';
|
|
5
|
+
import { useTranslation, formatCurrency, getSupabaseClientOptional } from '@fayz-ai/core';
|
|
6
|
+
import { getCoursesProvider } from '@fayz-ai/courses';
|
|
7
|
+
import { ImagePlus, Users, ShoppingCart, Layers, Tag, Loader2, UploadCloud, Info, Plus, Trash2, Clock, Lock, GripVertical, ChevronDown, Video, FileText, PlayCircle, Check } from 'lucide-react';
|
|
8
|
+
import { useSensors, useSensor, PointerSensor, KeyboardSensor, DndContext, closestCenter } from '@dnd-kit/core';
|
|
9
|
+
import { sortableKeyboardCoordinates, SortableContext, verticalListSortingStrategy, useSortable, arrayMove } from '@dnd-kit/sortable';
|
|
10
|
+
import { CSS } from '@dnd-kit/utilities';
|
|
11
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
12
|
+
|
|
13
|
+
function buildCourseStudents(orders) {
|
|
14
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
15
|
+
for (const o of orders) {
|
|
16
|
+
const key = o.customerId ?? o.customerEmail ?? o.id;
|
|
17
|
+
const existing = byKey.get(key);
|
|
18
|
+
if (!existing || o.createdAt && existing.since && o.createdAt > existing.since) {
|
|
19
|
+
byKey.set(key, {
|
|
20
|
+
key,
|
|
21
|
+
name: o.customerName ?? o.customerEmail ?? "\u2014",
|
|
22
|
+
email: o.customerEmail ?? null,
|
|
23
|
+
since: o.createdAt ?? null
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return [...byKey.values()].sort((a, b) => (b.since ?? "").localeCompare(a.since ?? ""));
|
|
28
|
+
}
|
|
29
|
+
var STATUSES = ["draft", "published", "archived"];
|
|
30
|
+
var DELIVERY_TYPES = ["online", "in_person", "hybrid"];
|
|
31
|
+
var TABS = ["details", "curriculum", "offers", "members"];
|
|
32
|
+
var TAB_ICON = {
|
|
33
|
+
details: Tag,
|
|
34
|
+
curriculum: Layers,
|
|
35
|
+
offers: ShoppingCart,
|
|
36
|
+
members: Users
|
|
37
|
+
};
|
|
38
|
+
function editSnapshot(c) {
|
|
39
|
+
return JSON.stringify({
|
|
40
|
+
title: c.title,
|
|
41
|
+
subtitle: c.subtitle,
|
|
42
|
+
description: c.description,
|
|
43
|
+
price: c.price,
|
|
44
|
+
status: c.status,
|
|
45
|
+
deliveryType: c.deliveryType,
|
|
46
|
+
thumbnailUrl: c.thumbnailUrl
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function CourseEditorPage({ id }) {
|
|
50
|
+
const t = useTranslation();
|
|
51
|
+
const [tab, setTab] = React.useState("details");
|
|
52
|
+
const [course, setCourse] = React.useState(null);
|
|
53
|
+
const [modules, setModules] = React.useState([]);
|
|
54
|
+
const [lessons, setLessons] = React.useState([]);
|
|
55
|
+
const [offers, setOffers] = React.useState([]);
|
|
56
|
+
const [students, setStudents] = React.useState([]);
|
|
57
|
+
const [loading, setLoading] = React.useState(true);
|
|
58
|
+
const [savedSnap, setSavedSnap] = React.useState("");
|
|
59
|
+
const [saving, setSaving] = React.useState(false);
|
|
60
|
+
const [uploading, setUploading] = React.useState(false);
|
|
61
|
+
const fileRef = React.useRef(null);
|
|
62
|
+
const [pendingDelete, setPendingDelete] = React.useState(null);
|
|
63
|
+
const [autoFocusId, setAutoFocusId] = React.useState(null);
|
|
64
|
+
const reload = React.useCallback(async () => {
|
|
65
|
+
if (!id) return;
|
|
66
|
+
const provider = getCoursesProvider();
|
|
67
|
+
const c = await provider.getCourse(id);
|
|
68
|
+
setCourse(c);
|
|
69
|
+
if (c) {
|
|
70
|
+
setSavedSnap(editSnapshot(c));
|
|
71
|
+
const [m, l, o, paidOrders] = await Promise.all([
|
|
72
|
+
provider.listModules(c.id),
|
|
73
|
+
provider.listLessons(c.id),
|
|
74
|
+
provider.listOffers(c.id),
|
|
75
|
+
provider.listOrders({ courseId: c.id, financialStatus: "paid" })
|
|
76
|
+
]);
|
|
77
|
+
setModules(m);
|
|
78
|
+
setLessons(l);
|
|
79
|
+
setOffers(o);
|
|
80
|
+
setStudents(buildCourseStudents(paidOrders));
|
|
81
|
+
}
|
|
82
|
+
setLoading(false);
|
|
83
|
+
}, [id]);
|
|
84
|
+
React.useEffect(() => {
|
|
85
|
+
void reload();
|
|
86
|
+
}, [reload]);
|
|
87
|
+
const dirty = !!course && editSnapshot(course) !== savedSnap;
|
|
88
|
+
useSaveBar({
|
|
89
|
+
dirty,
|
|
90
|
+
saving,
|
|
91
|
+
onSave: () => {
|
|
92
|
+
void saveDetails();
|
|
93
|
+
},
|
|
94
|
+
onDiscard: () => {
|
|
95
|
+
void reload();
|
|
96
|
+
},
|
|
97
|
+
saveLabel: t("courses.editor.save") || "Salvar"
|
|
98
|
+
});
|
|
99
|
+
const header = /* @__PURE__ */ jsx(
|
|
100
|
+
PageHeader,
|
|
101
|
+
{
|
|
102
|
+
title: course?.title ?? "",
|
|
103
|
+
parentLabel: t("courses.title") || "Courses",
|
|
104
|
+
onBack: () => navigateTo("/courses")
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
if (loading || !course) return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
108
|
+
header,
|
|
109
|
+
/* @__PURE__ */ jsx(EditorSkeleton, {})
|
|
110
|
+
] });
|
|
111
|
+
function patchCourse(patch) {
|
|
112
|
+
setCourse((c) => c ? { ...c, ...patch } : c);
|
|
113
|
+
}
|
|
114
|
+
async function saveDetails() {
|
|
115
|
+
if (!course) return;
|
|
116
|
+
setSaving(true);
|
|
117
|
+
try {
|
|
118
|
+
await getCoursesProvider().updateCourse(course.id, {
|
|
119
|
+
title: course.title,
|
|
120
|
+
subtitle: course.subtitle ?? void 0,
|
|
121
|
+
description: course.description ?? void 0,
|
|
122
|
+
thumbnailUrl: course.thumbnailUrl ?? void 0,
|
|
123
|
+
price: course.price,
|
|
124
|
+
status: course.status,
|
|
125
|
+
deliveryType: course.deliveryType
|
|
126
|
+
});
|
|
127
|
+
setSavedSnap(editSnapshot(course));
|
|
128
|
+
toast.success(t("courses.editor.saved") || "Course saved");
|
|
129
|
+
} catch {
|
|
130
|
+
toast.error(t("courses.editor.saveFailed") || "Could not save the course");
|
|
131
|
+
} finally {
|
|
132
|
+
setSaving(false);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function onPickThumbnail(e) {
|
|
136
|
+
const file = e.target.files?.[0];
|
|
137
|
+
if (!file || !course) return;
|
|
138
|
+
const supabase = getSupabaseClientOptional();
|
|
139
|
+
if (!supabase?.storage) {
|
|
140
|
+
toast.error(t("courses.editor.uploadUnavailable") || "Upload unavailable \u2014 paste an image URL instead.");
|
|
141
|
+
if (fileRef.current) fileRef.current.value = "";
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
setUploading(true);
|
|
145
|
+
try {
|
|
146
|
+
const ext = file.name.split(".").pop() || "jpg";
|
|
147
|
+
const path = `${course.id}/cover-${Date.now()}.${ext}`;
|
|
148
|
+
const bucket = supabase.storage.from("course-media");
|
|
149
|
+
const { error } = await bucket.upload(path, file, { upsert: true, contentType: file.type });
|
|
150
|
+
if (error) throw error;
|
|
151
|
+
const { data } = bucket.getPublicUrl(path);
|
|
152
|
+
patchCourse({ thumbnailUrl: data.publicUrl });
|
|
153
|
+
toast.success(t("courses.editor.thumbnailUploaded") || "Image uploaded \u2014 remember to save.");
|
|
154
|
+
} catch {
|
|
155
|
+
toast.error(t("courses.editor.uploadFailed") || "Could not upload the image");
|
|
156
|
+
} finally {
|
|
157
|
+
setUploading(false);
|
|
158
|
+
if (fileRef.current) fileRef.current.value = "";
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async function addModule() {
|
|
162
|
+
const mod = await getCoursesProvider().createModule({ courseId: course.id, title: t("courses.editor.untitledModule") || "New module" });
|
|
163
|
+
toast.success(t("courses.editor.moduleAdded") || "Module added");
|
|
164
|
+
await reload();
|
|
165
|
+
setAutoFocusId(mod.id);
|
|
166
|
+
}
|
|
167
|
+
async function renameModule(moduleId, title) {
|
|
168
|
+
await getCoursesProvider().updateModule(moduleId, { title });
|
|
169
|
+
}
|
|
170
|
+
async function addLesson(moduleId) {
|
|
171
|
+
const tempId = `tmp-${crypto.randomUUID()}`;
|
|
172
|
+
const nextOrder = lessons.filter((l) => l.moduleId === moduleId).reduce((max, l) => Math.max(max, l.sortOrder), -1) + 1;
|
|
173
|
+
const optimistic = {
|
|
174
|
+
id: tempId,
|
|
175
|
+
courseId: course.id,
|
|
176
|
+
moduleId,
|
|
177
|
+
title: t("courses.editor.untitledLesson") || "New lesson",
|
|
178
|
+
description: null,
|
|
179
|
+
videoUrl: "",
|
|
180
|
+
durationSec: 0,
|
|
181
|
+
sortOrder: nextOrder
|
|
182
|
+
};
|
|
183
|
+
setLessons((ls) => [...ls, optimistic]);
|
|
184
|
+
setAutoFocusId(tempId);
|
|
185
|
+
try {
|
|
186
|
+
const real = await getCoursesProvider().createLesson({
|
|
187
|
+
courseId: course.id,
|
|
188
|
+
moduleId,
|
|
189
|
+
title: optimistic.title,
|
|
190
|
+
videoUrl: ""
|
|
191
|
+
});
|
|
192
|
+
setLessons((ls) => ls.map((l) => l.id === tempId ? { ...l, id: real.id } : l));
|
|
193
|
+
setAutoFocusId((cur) => cur === tempId ? real.id : cur);
|
|
194
|
+
} catch {
|
|
195
|
+
setLessons((ls) => ls.filter((l) => l.id !== tempId));
|
|
196
|
+
toast.error(t("courses.editor.saveFailed") || "Could not add the lesson");
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async function patchLesson(lessonId, patch) {
|
|
200
|
+
await getCoursesProvider().updateLesson(lessonId, {
|
|
201
|
+
title: patch.title,
|
|
202
|
+
videoUrl: patch.videoUrl,
|
|
203
|
+
durationSec: patch.durationSec,
|
|
204
|
+
description: patch.description ?? void 0
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
async function reorderModules(orderedIds) {
|
|
208
|
+
const prev = modules;
|
|
209
|
+
setModules(orderedIds.map((mid, i) => ({ ...prev.find((m) => m.id === mid), sortOrder: i })));
|
|
210
|
+
try {
|
|
211
|
+
await Promise.all(orderedIds.map((mid, i) => getCoursesProvider().updateModule(mid, { sortOrder: i })));
|
|
212
|
+
} catch {
|
|
213
|
+
toast.error(t("courses.editor.reorderFailed") || "Could not reorder");
|
|
214
|
+
} finally {
|
|
215
|
+
await reload();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function reorderLessons(moduleId, orderedIds) {
|
|
219
|
+
const prev = lessons;
|
|
220
|
+
const others = prev.filter((l) => l.moduleId !== moduleId);
|
|
221
|
+
const reordered = orderedIds.map((lid, i) => ({ ...prev.find((l) => l.id === lid), sortOrder: i }));
|
|
222
|
+
setLessons([...others, ...reordered]);
|
|
223
|
+
try {
|
|
224
|
+
await Promise.all(orderedIds.map((lid, i) => getCoursesProvider().updateLesson(lid, { sortOrder: i })));
|
|
225
|
+
} catch {
|
|
226
|
+
toast.error(t("courses.editor.reorderFailed") || "Could not reorder");
|
|
227
|
+
} finally {
|
|
228
|
+
await reload();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async function confirmDelete() {
|
|
232
|
+
if (!pendingDelete) return;
|
|
233
|
+
const provider = getCoursesProvider();
|
|
234
|
+
if (pendingDelete.kind === "module") await provider.deleteModule(pendingDelete.id);
|
|
235
|
+
else await provider.deleteLesson(pendingDelete.id);
|
|
236
|
+
toast.success(t("courses.editor.deleted") || "Removed");
|
|
237
|
+
setPendingDelete(null);
|
|
238
|
+
await reload();
|
|
239
|
+
}
|
|
240
|
+
async function addOffer() {
|
|
241
|
+
await getCoursesProvider().createOffer({
|
|
242
|
+
courseId: course.id,
|
|
243
|
+
name: t("courses.offers.newName") || "New offer",
|
|
244
|
+
price: course.price,
|
|
245
|
+
kind: "one_time"
|
|
246
|
+
});
|
|
247
|
+
toast.success(t("courses.offers.added") || "Offer added");
|
|
248
|
+
await reload();
|
|
249
|
+
}
|
|
250
|
+
async function patchOffer(offerId, patch) {
|
|
251
|
+
await getCoursesProvider().updateOffer(offerId, {
|
|
252
|
+
name: patch.name,
|
|
253
|
+
price: patch.price,
|
|
254
|
+
kind: patch.kind,
|
|
255
|
+
recurringInterval: patch.recurringInterval,
|
|
256
|
+
isOrderBump: patch.isOrderBump,
|
|
257
|
+
isDefault: patch.isDefault
|
|
258
|
+
});
|
|
259
|
+
await reload();
|
|
260
|
+
}
|
|
261
|
+
async function deleteOffer(offerId) {
|
|
262
|
+
await getCoursesProvider().deleteOffer(offerId);
|
|
263
|
+
toast.success(t("courses.offers.removed") || "Offer removed");
|
|
264
|
+
await reload();
|
|
265
|
+
}
|
|
266
|
+
const deliveryOptions = DELIVERY_TYPES.map((d) => ({
|
|
267
|
+
value: d,
|
|
268
|
+
label: t(`courses.editor.delivery.${d}`) || d
|
|
269
|
+
}));
|
|
270
|
+
return (
|
|
271
|
+
// Sem padding e sem max-width próprios: a shell já envolve toda página em
|
|
272
|
+
// `p-3 md:p-6` (AdminShell). Reaplicar `p-6 md:p-8` aqui empurrava o editor
|
|
273
|
+
// 32px à direita do título na barra de topo e de todas as outras telas, e o
|
|
274
|
+
// `mx-auto max-w-4xl` ainda o centralizava enquanto a barra ficava à esquerda.
|
|
275
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
|
|
276
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-start gap-5", children: [
|
|
277
|
+
/* @__PURE__ */ jsx("div", { className: "aspect-video w-36 shrink-0 overflow-hidden rounded-2xl border border-border bg-muted shadow-sm", children: course.thumbnailUrl ? (
|
|
278
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
279
|
+
/* @__PURE__ */ jsx("img", { src: course.thumbnailUrl, alt: "", className: "h-full w-full object-cover" })
|
|
280
|
+
) : /* @__PURE__ */ jsx("div", { className: "flex h-full w-full items-center justify-center text-muted-foreground", children: /* @__PURE__ */ jsx(ImagePlus, { className: "h-6 w-6" }) }) }),
|
|
281
|
+
/* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-2", children: [
|
|
282
|
+
/* @__PURE__ */ jsx("h1", { className: "truncate text-2xl font-bold text-foreground", children: course.title || (t("courses.editor.untitledCourse") || "Untitled course") }),
|
|
283
|
+
/* @__PURE__ */ jsx(Badge, { variant: course.status === "published" ? "success" : course.status === "archived" ? "secondary" : "outline", children: t(`courses.status.${course.status}`) || course.status }),
|
|
284
|
+
/* @__PURE__ */ jsx(Badge, { variant: "outline", children: t(`courses.editor.delivery.${course.deliveryType}`) || course.deliveryType })
|
|
285
|
+
] }) })
|
|
286
|
+
] }),
|
|
287
|
+
/* @__PURE__ */ jsx(Separator, {}),
|
|
288
|
+
/* @__PURE__ */ jsxs(Tabs, { value: tab, onValueChange: (v) => setTab(v), children: [
|
|
289
|
+
/* @__PURE__ */ jsx(TabsList, { className: "max-w-full justify-start overflow-x-auto scrollbar-none", children: TABS.map((tk) => {
|
|
290
|
+
const Icon = TAB_ICON[tk];
|
|
291
|
+
return /* @__PURE__ */ jsxs(TabsTrigger, { value: tk, className: "gap-1.5", children: [
|
|
292
|
+
/* @__PURE__ */ jsx(Icon, { className: "h-3.5 w-3.5" }),
|
|
293
|
+
t(`courses.editor.tab.${tk}`) || tk
|
|
294
|
+
] }, tk);
|
|
295
|
+
}) }),
|
|
296
|
+
/* @__PURE__ */ jsxs(TabsContent, { value: "details", className: "mt-4 space-y-5", children: [
|
|
297
|
+
/* @__PURE__ */ jsx(SectionCard, { title: t("courses.editor.sectionBasics") || "Basics", children: /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
298
|
+
/* @__PURE__ */ jsx(Field, { label: t("courses.editor.fieldTitle") || "Title", children: /* @__PURE__ */ jsx(Input, { value: course.title, onChange: (e) => patchCourse({ title: e.target.value }) }) }),
|
|
299
|
+
/* @__PURE__ */ jsx(Field, { label: t("courses.editor.fieldSubtitle") || "Subtitle", children: /* @__PURE__ */ jsx(Input, { value: course.subtitle ?? "", onChange: (e) => patchCourse({ subtitle: e.target.value }) }) }),
|
|
300
|
+
/* @__PURE__ */ jsx(Field, { label: t("courses.editor.fieldDescription") || "Description", children: /* @__PURE__ */ jsx(
|
|
301
|
+
Textarea,
|
|
302
|
+
{
|
|
303
|
+
className: "min-h-[96px] px-2 py-1.5",
|
|
304
|
+
value: course.description ?? "",
|
|
305
|
+
onChange: (e) => patchCourse({ description: e.target.value }),
|
|
306
|
+
placeholder: t("courses.editor.descriptionPlaceholder") || "What will students learn?"
|
|
307
|
+
}
|
|
308
|
+
) }),
|
|
309
|
+
/* @__PURE__ */ jsx(Field, { label: t("courses.editor.fieldDelivery") || "Format", hint: t("courses.editor.deliveryHint") || "How this course is delivered.", children: /* @__PURE__ */ jsx(
|
|
310
|
+
SegmentedControl,
|
|
311
|
+
{
|
|
312
|
+
size: "md",
|
|
313
|
+
options: deliveryOptions,
|
|
314
|
+
value: course.deliveryType,
|
|
315
|
+
onChange: (v) => patchCourse({ deliveryType: v }),
|
|
316
|
+
"aria-label": t("courses.editor.fieldDelivery") || "Format"
|
|
317
|
+
}
|
|
318
|
+
) })
|
|
319
|
+
] }) }),
|
|
320
|
+
/* @__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: [
|
|
321
|
+
/* @__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 ? (
|
|
322
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
323
|
+
/* @__PURE__ */ jsx("img", { src: course.thumbnailUrl, alt: "", className: "h-full w-full object-cover" })
|
|
324
|
+
) : /* @__PURE__ */ jsxs("div", { className: "flex h-full w-full flex-col items-center justify-center gap-1 text-muted-foreground", children: [
|
|
325
|
+
/* @__PURE__ */ jsx(ImagePlus, { className: "h-6 w-6" }),
|
|
326
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs", children: t("courses.editor.thumbnailEmpty") || "No image" })
|
|
327
|
+
] }) }),
|
|
328
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 space-y-3", children: [
|
|
329
|
+
/* @__PURE__ */ jsx("input", { ref: fileRef, type: "file", accept: "image/*", className: "hidden", onChange: onPickThumbnail }),
|
|
330
|
+
/* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", disabled: uploading, onClick: () => fileRef.current?.click(), children: [
|
|
331
|
+
uploading ? /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsx(UploadCloud, { className: "h-4 w-4" }),
|
|
332
|
+
uploading ? t("courses.editor.uploading") || "Uploading\u2026" : t("courses.editor.thumbnailUpload") || "Upload image"
|
|
333
|
+
] }),
|
|
334
|
+
/* @__PURE__ */ jsx(Field, { label: t("courses.editor.thumbnailUrl") || "Or paste an image URL", children: /* @__PURE__ */ jsx(
|
|
335
|
+
Input,
|
|
336
|
+
{
|
|
337
|
+
value: course.thumbnailUrl ?? "",
|
|
338
|
+
placeholder: "https://\u2026",
|
|
339
|
+
onChange: (e) => patchCourse({ thumbnailUrl: e.target.value || null })
|
|
340
|
+
}
|
|
341
|
+
) })
|
|
342
|
+
] })
|
|
343
|
+
] }) }),
|
|
344
|
+
/* @__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: [
|
|
345
|
+
/* @__PURE__ */ jsx(Field, { label: t("courses.editor.fieldPrice") || "Price", hint: course.currency, children: /* @__PURE__ */ jsx(
|
|
346
|
+
Input,
|
|
347
|
+
{
|
|
348
|
+
type: "number",
|
|
349
|
+
min: 0,
|
|
350
|
+
step: "0.01",
|
|
351
|
+
inputMode: "decimal",
|
|
352
|
+
value: String(course.price),
|
|
353
|
+
onChange: (e) => patchCourse({ price: Number(e.target.value) || 0 })
|
|
354
|
+
}
|
|
355
|
+
) }),
|
|
356
|
+
/* @__PURE__ */ jsx(Field, { label: t("courses.editor.fieldStatus") || "Status", children: /* @__PURE__ */ jsxs(Select, { value: course.status, onValueChange: (v) => patchCourse({ status: v }), children: [
|
|
357
|
+
/* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
358
|
+
/* @__PURE__ */ jsx(SelectContent, { children: STATUSES.map((s) => /* @__PURE__ */ jsx(SelectItem, { value: s, children: t(`courses.status.${s}`) || s }, s)) })
|
|
359
|
+
] }) })
|
|
360
|
+
] }) })
|
|
361
|
+
] }),
|
|
362
|
+
/* @__PURE__ */ jsx(TabsContent, { value: "curriculum", className: "mt-4 space-y-4", children: /* @__PURE__ */ jsx(
|
|
363
|
+
CurriculumBuilder,
|
|
364
|
+
{
|
|
365
|
+
t,
|
|
366
|
+
modules,
|
|
367
|
+
lessons,
|
|
368
|
+
autoFocusId,
|
|
369
|
+
onAutoFocused: () => setAutoFocusId(null),
|
|
370
|
+
onAddModule: addModule,
|
|
371
|
+
onRenameModule: renameModule,
|
|
372
|
+
onAddLesson: addLesson,
|
|
373
|
+
onPatchLesson: patchLesson,
|
|
374
|
+
onRequestDelete: (kind, delId) => setPendingDelete({ kind, id: delId }),
|
|
375
|
+
onReorderModules: reorderModules,
|
|
376
|
+
onReorderLessons: reorderLessons
|
|
377
|
+
}
|
|
378
|
+
) }),
|
|
379
|
+
/* @__PURE__ */ jsxs(TabsContent, { value: "offers", className: "mt-4 space-y-4", children: [
|
|
380
|
+
/* @__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." }),
|
|
381
|
+
offers.length === 0 ? /* @__PURE__ */ jsx(
|
|
382
|
+
EmptyState,
|
|
383
|
+
{
|
|
384
|
+
icon: /* @__PURE__ */ jsx(Tag, { className: "h-5 w-5" }),
|
|
385
|
+
title: t("courses.offers.empty") || "No offers yet",
|
|
386
|
+
description: t("courses.offers.emptyHint") || "Create an offer to sell this course.",
|
|
387
|
+
action: /* @__PURE__ */ jsxs(Button, { onClick: addOffer, children: [
|
|
388
|
+
/* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" }),
|
|
389
|
+
t("courses.offers.firstOffer") || "Create first offer"
|
|
390
|
+
] })
|
|
391
|
+
}
|
|
392
|
+
) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
393
|
+
offers.map((offer) => /* @__PURE__ */ jsxs("div", { className: "rounded-card border border-border bg-card p-4 shadow-sm md:p-5", children: [
|
|
394
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-4 flex items-start justify-between gap-3", children: [
|
|
395
|
+
/* @__PURE__ */ jsx(Badge, { variant: "outline", className: "gap-1", children: offer.kind === "subscription" ? t("courses.offers.subscription") || "Subscription" : t("courses.offers.oneTime") || "One-time" }),
|
|
396
|
+
/* @__PURE__ */ jsx(
|
|
397
|
+
Button,
|
|
398
|
+
{
|
|
399
|
+
variant: "bare",
|
|
400
|
+
size: "bare",
|
|
401
|
+
onClick: () => deleteOffer(offer.id),
|
|
402
|
+
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",
|
|
403
|
+
"aria-label": t("courses.offers.remove") || "Remove",
|
|
404
|
+
children: /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4" })
|
|
405
|
+
}
|
|
406
|
+
)
|
|
407
|
+
] }),
|
|
408
|
+
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-[1fr_140px_160px]", children: [
|
|
409
|
+
/* @__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 }) }) }),
|
|
410
|
+
/* @__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 }) }) }),
|
|
411
|
+
/* @__PURE__ */ jsx(Field, { label: t("courses.offers.kind") || "Type", children: /* @__PURE__ */ jsxs(
|
|
412
|
+
Select,
|
|
413
|
+
{
|
|
414
|
+
value: offer.kind,
|
|
415
|
+
onValueChange: (v) => patchOffer(offer.id, { kind: v, recurringInterval: v === "subscription" ? offer.recurringInterval ?? "month" : null }),
|
|
416
|
+
children: [
|
|
417
|
+
/* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
418
|
+
/* @__PURE__ */ jsxs(SelectContent, { children: [
|
|
419
|
+
/* @__PURE__ */ jsx(SelectItem, { value: "one_time", children: t("courses.offers.oneTime") || "One-time" }),
|
|
420
|
+
/* @__PURE__ */ jsx(SelectItem, { value: "subscription", children: t("courses.offers.subscription") || "Subscription" })
|
|
421
|
+
] })
|
|
422
|
+
]
|
|
423
|
+
}
|
|
424
|
+
) })
|
|
425
|
+
] }),
|
|
426
|
+
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(
|
|
427
|
+
Select,
|
|
428
|
+
{
|
|
429
|
+
value: offer.recurringInterval ?? "month",
|
|
430
|
+
onValueChange: (v) => patchOffer(offer.id, { recurringInterval: v }),
|
|
431
|
+
children: [
|
|
432
|
+
/* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
433
|
+
/* @__PURE__ */ jsxs(SelectContent, { children: [
|
|
434
|
+
/* @__PURE__ */ jsx(SelectItem, { value: "month", children: t("courses.interval.month") || "month" }),
|
|
435
|
+
/* @__PURE__ */ jsx(SelectItem, { value: "year", children: t("courses.interval.year") || "year" })
|
|
436
|
+
] })
|
|
437
|
+
]
|
|
438
|
+
}
|
|
439
|
+
) }) }),
|
|
440
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-4 rounded-lg border border-dashed border-border bg-muted/30 p-3", children: [
|
|
441
|
+
/* @__PURE__ */ jsxs("p", { className: "mb-2.5 flex items-center gap-1.5 text-xs text-muted-foreground", children: [
|
|
442
|
+
/* @__PURE__ */ jsx(Clock, { className: "h-3.5 w-3.5" }),
|
|
443
|
+
t("courses.offers.checkoutFlagsNote") || "Apply once the public checkout ships (coming soon)."
|
|
444
|
+
] }),
|
|
445
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-5 gap-y-2", children: [
|
|
446
|
+
/* @__PURE__ */ jsxs("label", { className: "flex cursor-pointer items-center gap-2 text-sm text-muted-foreground", children: [
|
|
447
|
+
/* @__PURE__ */ jsx(Checkbox, { checked: offer.isDefault, onChange: (v) => patchOffer(offer.id, { isDefault: v }) }),
|
|
448
|
+
t("courses.offers.default") || "Default"
|
|
449
|
+
] }),
|
|
450
|
+
/* @__PURE__ */ jsxs("label", { className: "flex cursor-pointer items-center gap-2 text-sm text-muted-foreground", children: [
|
|
451
|
+
/* @__PURE__ */ jsx(Checkbox, { checked: offer.isOrderBump, onChange: (v) => patchOffer(offer.id, { isOrderBump: v }) }),
|
|
452
|
+
t("courses.offers.bump") || "Order bump"
|
|
453
|
+
] })
|
|
454
|
+
] })
|
|
455
|
+
] })
|
|
456
|
+
] }, offer.id)),
|
|
457
|
+
/* @__PURE__ */ jsxs(Button, { variant: "outline", onClick: addOffer, className: "w-full border-dashed", children: [
|
|
458
|
+
/* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" }),
|
|
459
|
+
t("courses.offers.add") || "Add offer"
|
|
460
|
+
] })
|
|
461
|
+
] })
|
|
462
|
+
] }),
|
|
463
|
+
/* @__PURE__ */ jsxs(TabsContent, { value: "members", className: "mt-4 space-y-4", children: [
|
|
464
|
+
/* @__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." }),
|
|
465
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
466
|
+
/* @__PURE__ */ jsxs("p", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: [
|
|
467
|
+
t("courses.editor.enrolledStudents") || "Enrolled students",
|
|
468
|
+
" (",
|
|
469
|
+
students.length,
|
|
470
|
+
")"
|
|
471
|
+
] }),
|
|
472
|
+
students.length === 0 ? /* @__PURE__ */ jsx(
|
|
473
|
+
EmptyState,
|
|
474
|
+
{
|
|
475
|
+
icon: /* @__PURE__ */ jsx(Users, { className: "h-5 w-5" }),
|
|
476
|
+
title: t("courses.editor.membersEmpty") || "No students yet",
|
|
477
|
+
description: t("courses.editor.membersEmptyHint") || "Buyers are enrolled here automatically after a paid order."
|
|
478
|
+
}
|
|
479
|
+
) : /* @__PURE__ */ jsx("div", { className: "divide-y divide-border overflow-hidden rounded-card border border-border bg-card", children: students.map((s) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-4 py-3", children: [
|
|
480
|
+
/* @__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) }),
|
|
481
|
+
/* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [
|
|
482
|
+
/* @__PURE__ */ jsx("span", { className: "truncate font-medium text-foreground", children: s.name }),
|
|
483
|
+
s.email && /* @__PURE__ */ jsx("span", { className: "truncate text-xs text-muted-foreground", children: s.email })
|
|
484
|
+
] }),
|
|
485
|
+
s.since && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs tabular-nums text-muted-foreground", children: new Date(s.since).toLocaleDateString() })
|
|
486
|
+
] }, s.key)) })
|
|
487
|
+
] }),
|
|
488
|
+
offers.length > 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
489
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: t("courses.editor.accessUnlockedBy") || "Access is unlocked by" }),
|
|
490
|
+
/* @__PURE__ */ jsx("div", { className: "divide-y divide-border overflow-hidden rounded-card border border-border bg-card", children: offers.map((offer) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-4 py-3", children: [
|
|
491
|
+
/* @__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" }) }),
|
|
492
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate font-medium text-foreground", children: offer.name }),
|
|
493
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-sm tabular-nums text-muted-foreground", children: formatCurrency(offer.price, offer.currency) })
|
|
494
|
+
] }, offer.id)) })
|
|
495
|
+
] })
|
|
496
|
+
] })
|
|
497
|
+
] }),
|
|
498
|
+
/* @__PURE__ */ jsx(
|
|
499
|
+
ConfirmDialog,
|
|
500
|
+
{
|
|
501
|
+
open: pendingDelete !== null,
|
|
502
|
+
variant: "destructive",
|
|
503
|
+
title: pendingDelete?.kind === "module" ? t("courses.editor.deleteModuleConfirm") || "Delete this module?" : t("courses.editor.deleteLessonConfirm") || "Remove this lesson?",
|
|
504
|
+
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.",
|
|
505
|
+
confirmLabel: t("courses.editor.confirmDelete") || "Delete",
|
|
506
|
+
cancelLabel: t("courses.editor.cancel") || "Cancel",
|
|
507
|
+
onConfirm: confirmDelete,
|
|
508
|
+
onCancel: () => setPendingDelete(null)
|
|
509
|
+
}
|
|
510
|
+
)
|
|
511
|
+
] })
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
CourseEditorPage.displayName = "CourseEditorPage";
|
|
515
|
+
function formatDuration(sec, t) {
|
|
516
|
+
const min = Math.max(0, Math.round(sec / 60));
|
|
517
|
+
const mLabel = t("courses.editor.minShort") || "min";
|
|
518
|
+
if (min < 60) return `${min} ${mLabel}`;
|
|
519
|
+
const h = Math.floor(min / 60);
|
|
520
|
+
const rem = min % 60;
|
|
521
|
+
const hLabel = t("courses.editor.hourShort") || "h";
|
|
522
|
+
return rem ? `${h} ${hLabel} ${rem} ${mLabel}` : `${h} ${hLabel}`;
|
|
523
|
+
}
|
|
524
|
+
function CurriculumBuilder({
|
|
525
|
+
t,
|
|
526
|
+
modules,
|
|
527
|
+
lessons,
|
|
528
|
+
autoFocusId,
|
|
529
|
+
onAutoFocused,
|
|
530
|
+
onAddModule,
|
|
531
|
+
onRenameModule,
|
|
532
|
+
onAddLesson,
|
|
533
|
+
onPatchLesson,
|
|
534
|
+
onRequestDelete,
|
|
535
|
+
onReorderModules,
|
|
536
|
+
onReorderLessons
|
|
537
|
+
}) {
|
|
538
|
+
const [collapsed, setCollapsed] = React.useState(/* @__PURE__ */ new Set());
|
|
539
|
+
const sensors = useSensors(
|
|
540
|
+
useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
|
|
541
|
+
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
|
|
542
|
+
);
|
|
543
|
+
function toggleModule(id) {
|
|
544
|
+
setCollapsed((prev) => {
|
|
545
|
+
const next = new Set(prev);
|
|
546
|
+
if (next.has(id)) next.delete(id);
|
|
547
|
+
else next.add(id);
|
|
548
|
+
return next;
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
function handleModuleDragEnd(e) {
|
|
552
|
+
const { active, over } = e;
|
|
553
|
+
if (!over || active.id === over.id) return;
|
|
554
|
+
const ids = modules.map((m) => m.id);
|
|
555
|
+
const from = ids.indexOf(String(active.id));
|
|
556
|
+
const to = ids.indexOf(String(over.id));
|
|
557
|
+
if (from < 0 || to < 0) return;
|
|
558
|
+
onReorderModules(arrayMove(ids, from, to));
|
|
559
|
+
}
|
|
560
|
+
if (modules.length === 0) {
|
|
561
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
562
|
+
/* @__PURE__ */ jsx(AutosaveHint, { t }),
|
|
563
|
+
/* @__PURE__ */ jsx(
|
|
564
|
+
EmptyState,
|
|
565
|
+
{
|
|
566
|
+
icon: /* @__PURE__ */ jsx(Layers, { className: "h-5 w-5" }),
|
|
567
|
+
title: t("courses.editor.modulesEmpty") || "No modules yet",
|
|
568
|
+
description: t("courses.editor.modulesEmptyHint") || "Create a module to start organizing your lessons.",
|
|
569
|
+
action: /* @__PURE__ */ jsxs(Button, { onClick: onAddModule, children: [
|
|
570
|
+
/* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" }),
|
|
571
|
+
t("courses.editor.firstModule") || "Create first module"
|
|
572
|
+
] })
|
|
573
|
+
}
|
|
574
|
+
)
|
|
575
|
+
] });
|
|
576
|
+
}
|
|
577
|
+
const totalLessons = lessons.length;
|
|
578
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
579
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [
|
|
580
|
+
/* @__PURE__ */ jsx(AutosaveHint, { t }),
|
|
581
|
+
/* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground tabular-nums", children: [
|
|
582
|
+
modules.length,
|
|
583
|
+
" ",
|
|
584
|
+
modules.length === 1 ? t("courses.editor.moduleLabel") || "module" : t("courses.editor.moduleLabelPlural") || "modules",
|
|
585
|
+
" \xB7 ",
|
|
586
|
+
totalLessons,
|
|
587
|
+
" ",
|
|
588
|
+
totalLessons === 1 ? t("courses.editor.lessonUnitOne") || "lesson" : t("courses.editor.lessonUnitOther") || "lessons"
|
|
589
|
+
] })
|
|
590
|
+
] }),
|
|
591
|
+
/* @__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(
|
|
592
|
+
SortableModuleCard,
|
|
593
|
+
{
|
|
594
|
+
t,
|
|
595
|
+
index: i,
|
|
596
|
+
mod,
|
|
597
|
+
lessons: lessons.filter((l) => l.moduleId === mod.id),
|
|
598
|
+
expanded: !collapsed.has(mod.id),
|
|
599
|
+
onToggle: () => toggleModule(mod.id),
|
|
600
|
+
autoFocusId,
|
|
601
|
+
onAutoFocused,
|
|
602
|
+
onRenameModule,
|
|
603
|
+
onAddLesson,
|
|
604
|
+
onPatchLesson,
|
|
605
|
+
onRequestDelete,
|
|
606
|
+
onReorderLessons
|
|
607
|
+
},
|
|
608
|
+
mod.id
|
|
609
|
+
)) }) }) }),
|
|
610
|
+
/* @__PURE__ */ jsxs(Button, { variant: "outline", onClick: onAddModule, className: "w-full border-dashed", children: [
|
|
611
|
+
/* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" }),
|
|
612
|
+
t("courses.editor.newModule") || "New module"
|
|
613
|
+
] })
|
|
614
|
+
] });
|
|
615
|
+
}
|
|
616
|
+
function SortableModuleCard({
|
|
617
|
+
t,
|
|
618
|
+
index,
|
|
619
|
+
mod,
|
|
620
|
+
lessons,
|
|
621
|
+
expanded,
|
|
622
|
+
onToggle,
|
|
623
|
+
autoFocusId,
|
|
624
|
+
onAutoFocused,
|
|
625
|
+
onRenameModule,
|
|
626
|
+
onAddLesson,
|
|
627
|
+
onPatchLesson,
|
|
628
|
+
onRequestDelete,
|
|
629
|
+
onReorderLessons
|
|
630
|
+
}) {
|
|
631
|
+
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: mod.id });
|
|
632
|
+
const sensors = useSensors(
|
|
633
|
+
useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
|
|
634
|
+
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
|
|
635
|
+
);
|
|
636
|
+
const style = {
|
|
637
|
+
transform: CSS.Transform.toString(transform),
|
|
638
|
+
transition,
|
|
639
|
+
opacity: isDragging ? 0.6 : 1,
|
|
640
|
+
zIndex: isDragging ? 20 : void 0
|
|
641
|
+
};
|
|
642
|
+
const count = lessons.length;
|
|
643
|
+
const totalSec = lessons.reduce((s, l) => s + (l.durationSec || 0), 0);
|
|
644
|
+
function handleLessonDragEnd(e) {
|
|
645
|
+
const { active, over } = e;
|
|
646
|
+
if (!over || active.id === over.id) return;
|
|
647
|
+
const ids = lessons.map((l) => l.id);
|
|
648
|
+
const from = ids.indexOf(String(active.id));
|
|
649
|
+
const to = ids.indexOf(String(over.id));
|
|
650
|
+
if (from < 0 || to < 0) return;
|
|
651
|
+
onReorderLessons(mod.id, arrayMove(ids, from, to));
|
|
652
|
+
}
|
|
653
|
+
return /* @__PURE__ */ jsxs(
|
|
654
|
+
"div",
|
|
655
|
+
{
|
|
656
|
+
ref: setNodeRef,
|
|
657
|
+
style,
|
|
658
|
+
className: "overflow-hidden rounded-card border border-border bg-card shadow-sm",
|
|
659
|
+
children: [
|
|
660
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 px-2 py-2 sm:gap-2 sm:px-3", children: [
|
|
661
|
+
/* @__PURE__ */ jsx(
|
|
662
|
+
Button,
|
|
663
|
+
{
|
|
664
|
+
variant: "bare",
|
|
665
|
+
size: "bare",
|
|
666
|
+
...attributes,
|
|
667
|
+
...listeners,
|
|
668
|
+
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",
|
|
669
|
+
"aria-label": t("courses.editor.dragToReorder") || "Drag to reorder",
|
|
670
|
+
children: /* @__PURE__ */ jsx(GripVertical, { className: "h-4 w-4" })
|
|
671
|
+
}
|
|
672
|
+
),
|
|
673
|
+
/* @__PURE__ */ jsx(
|
|
674
|
+
Button,
|
|
675
|
+
{
|
|
676
|
+
variant: "bare",
|
|
677
|
+
size: "bare",
|
|
678
|
+
onClick: onToggle,
|
|
679
|
+
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",
|
|
680
|
+
"aria-label": expanded ? t("courses.editor.collapse") || "Collapse" : t("courses.editor.expand") || "Expand",
|
|
681
|
+
"aria-expanded": expanded,
|
|
682
|
+
children: /* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4 transition-transform " + (expanded ? "" : "-rotate-90") })
|
|
683
|
+
}
|
|
684
|
+
),
|
|
685
|
+
/* @__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") }),
|
|
686
|
+
/* @__PURE__ */ jsx(
|
|
687
|
+
TitleField,
|
|
688
|
+
{
|
|
689
|
+
value: mod.title,
|
|
690
|
+
autoFocus: autoFocusId === mod.id,
|
|
691
|
+
onAutoFocused,
|
|
692
|
+
onCommit: (v) => onRenameModule(mod.id, v),
|
|
693
|
+
placeholder: t("courses.editor.moduleTitle") || "Module title",
|
|
694
|
+
ariaLabel: t("courses.editor.moduleTitle") || "Module title",
|
|
695
|
+
className: "text-sm font-semibold"
|
|
696
|
+
}
|
|
697
|
+
),
|
|
698
|
+
/* @__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" }),
|
|
699
|
+
/* @__PURE__ */ jsx(
|
|
700
|
+
Button,
|
|
701
|
+
{
|
|
702
|
+
variant: "bare",
|
|
703
|
+
size: "bare",
|
|
704
|
+
onClick: () => onRequestDelete("module", mod.id),
|
|
705
|
+
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",
|
|
706
|
+
"aria-label": t("courses.editor.deleteModule") || "Delete module",
|
|
707
|
+
children: /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4" })
|
|
708
|
+
}
|
|
709
|
+
)
|
|
710
|
+
] }),
|
|
711
|
+
expanded && /* @__PURE__ */ jsxs("div", { className: "border-t border-border/60 bg-muted/20 p-2 sm:p-3", children: [
|
|
712
|
+
count === 0 ? /* @__PURE__ */ jsxs(
|
|
713
|
+
Button,
|
|
714
|
+
{
|
|
715
|
+
variant: "bare",
|
|
716
|
+
size: "bare",
|
|
717
|
+
onClick: () => onAddLesson(mod.id),
|
|
718
|
+
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 font-normal",
|
|
719
|
+
children: [
|
|
720
|
+
/* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" }),
|
|
721
|
+
" ",
|
|
722
|
+
t("courses.editor.firstLesson") || "Add the first lesson"
|
|
723
|
+
]
|
|
724
|
+
}
|
|
725
|
+
) : /* @__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(
|
|
726
|
+
SortableLessonRow,
|
|
727
|
+
{
|
|
728
|
+
t,
|
|
729
|
+
lesson,
|
|
730
|
+
autoFocusId,
|
|
731
|
+
onAutoFocused,
|
|
732
|
+
onPatchLesson,
|
|
733
|
+
onRequestDelete
|
|
734
|
+
},
|
|
735
|
+
lesson.id
|
|
736
|
+
)) }) }) }),
|
|
737
|
+
count > 0 && /* @__PURE__ */ jsxs(
|
|
738
|
+
Button,
|
|
739
|
+
{
|
|
740
|
+
variant: "bare",
|
|
741
|
+
size: "bare",
|
|
742
|
+
onClick: () => onAddLesson(mod.id),
|
|
743
|
+
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",
|
|
744
|
+
children: [
|
|
745
|
+
/* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" }),
|
|
746
|
+
" ",
|
|
747
|
+
t("courses.editor.newLesson") || "New lesson"
|
|
748
|
+
]
|
|
749
|
+
}
|
|
750
|
+
)
|
|
751
|
+
] })
|
|
752
|
+
]
|
|
753
|
+
}
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
function SortableLessonRow({ t, lesson, autoFocusId, onAutoFocused, onPatchLesson, onRequestDelete }) {
|
|
757
|
+
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: lesson.id });
|
|
758
|
+
const [open, setOpen] = React.useState(false);
|
|
759
|
+
const [videoUrl, setVideoUrl] = React.useState(lesson.videoUrl);
|
|
760
|
+
const [durationSec, setDurationSec] = React.useState(lesson.durationSec);
|
|
761
|
+
React.useEffect(() => {
|
|
762
|
+
setVideoUrl(lesson.videoUrl);
|
|
763
|
+
}, [lesson.videoUrl]);
|
|
764
|
+
React.useEffect(() => {
|
|
765
|
+
setDurationSec(lesson.durationSec);
|
|
766
|
+
}, [lesson.durationSec]);
|
|
767
|
+
const hasVideo = videoUrl.trim().length > 0;
|
|
768
|
+
const style = {
|
|
769
|
+
transform: CSS.Transform.toString(transform),
|
|
770
|
+
transition,
|
|
771
|
+
opacity: isDragging ? 0.6 : 1,
|
|
772
|
+
zIndex: isDragging ? 10 : void 0
|
|
773
|
+
};
|
|
774
|
+
return /* @__PURE__ */ jsxs(
|
|
775
|
+
"div",
|
|
776
|
+
{
|
|
777
|
+
ref: setNodeRef,
|
|
778
|
+
style,
|
|
779
|
+
className: "group rounded-card border bg-card transition-colors " + (open ? "border-border" : "border-transparent hover:border-border"),
|
|
780
|
+
children: [
|
|
781
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 px-1.5 py-1.5 sm:gap-2 sm:px-2", children: [
|
|
782
|
+
/* @__PURE__ */ jsx(
|
|
783
|
+
Button,
|
|
784
|
+
{
|
|
785
|
+
variant: "bare",
|
|
786
|
+
size: "bare",
|
|
787
|
+
...attributes,
|
|
788
|
+
...listeners,
|
|
789
|
+
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",
|
|
790
|
+
"aria-label": t("courses.editor.dragToReorder") || "Drag to reorder",
|
|
791
|
+
children: /* @__PURE__ */ jsx(GripVertical, { className: "h-4 w-4" })
|
|
792
|
+
}
|
|
793
|
+
),
|
|
794
|
+
/* @__PURE__ */ jsx(
|
|
795
|
+
"span",
|
|
796
|
+
{
|
|
797
|
+
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"),
|
|
798
|
+
"aria-hidden": true,
|
|
799
|
+
children: hasVideo ? /* @__PURE__ */ jsx(Video, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(FileText, { className: "h-4 w-4" })
|
|
800
|
+
}
|
|
801
|
+
),
|
|
802
|
+
/* @__PURE__ */ jsx(
|
|
803
|
+
TitleField,
|
|
804
|
+
{
|
|
805
|
+
value: lesson.title,
|
|
806
|
+
autoFocus: autoFocusId === lesson.id,
|
|
807
|
+
onAutoFocused,
|
|
808
|
+
onCommit: (v) => onPatchLesson(lesson.id, { title: v }),
|
|
809
|
+
placeholder: t("courses.editor.lessonTitle") || "Lesson title",
|
|
810
|
+
ariaLabel: t("courses.editor.lessonTitle") || "Lesson title",
|
|
811
|
+
className: "text-sm"
|
|
812
|
+
}
|
|
813
|
+
),
|
|
814
|
+
/* @__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: [
|
|
815
|
+
/* @__PURE__ */ jsx(Clock, { className: "h-3 w-3" }),
|
|
816
|
+
Math.round(durationSec / 60),
|
|
817
|
+
" ",
|
|
818
|
+
t("courses.editor.minShort") || "min"
|
|
819
|
+
] }),
|
|
820
|
+
/* @__PURE__ */ jsxs(
|
|
821
|
+
Button,
|
|
822
|
+
{
|
|
823
|
+
variant: "bare",
|
|
824
|
+
size: "bare",
|
|
825
|
+
onClick: () => setOpen((v) => !v),
|
|
826
|
+
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" : ""),
|
|
827
|
+
"aria-label": t("courses.editor.editLesson") || "Edit lesson",
|
|
828
|
+
"aria-expanded": open,
|
|
829
|
+
children: [
|
|
830
|
+
/* @__PURE__ */ jsx(PlayCircle, { className: "h-3.5 w-3.5" }),
|
|
831
|
+
/* @__PURE__ */ jsx("span", { className: "hidden sm:inline", children: hasVideo ? t("courses.editor.videoSet") || "Video" : t("courses.editor.addVideo") || "Add video" })
|
|
832
|
+
]
|
|
833
|
+
}
|
|
834
|
+
),
|
|
835
|
+
/* @__PURE__ */ jsx(
|
|
836
|
+
Button,
|
|
837
|
+
{
|
|
838
|
+
variant: "bare",
|
|
839
|
+
size: "bare",
|
|
840
|
+
onClick: () => onRequestDelete("lesson", lesson.id),
|
|
841
|
+
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",
|
|
842
|
+
"aria-label": t("courses.editor.deleteLesson") || "Remove lesson",
|
|
843
|
+
children: /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4" })
|
|
844
|
+
}
|
|
845
|
+
)
|
|
846
|
+
] }),
|
|
847
|
+
open && /* @__PURE__ */ jsxs("div", { className: "space-y-3 border-t border-border/60 px-3 py-3 sm:pl-11", children: [
|
|
848
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
|
|
849
|
+
/* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("courses.editor.videoUrlLabel") || "Video URL" }),
|
|
850
|
+
/* @__PURE__ */ jsx(
|
|
851
|
+
Input,
|
|
852
|
+
{
|
|
853
|
+
defaultValue: lesson.videoUrl,
|
|
854
|
+
placeholder: t("courses.editor.videoUrlPlaceholder") || "Paste the video link (YouTube, Vimeo\u2026)",
|
|
855
|
+
onBlur: (e) => {
|
|
856
|
+
setVideoUrl(e.target.value);
|
|
857
|
+
onPatchLesson(lesson.id, { videoUrl: e.target.value });
|
|
858
|
+
},
|
|
859
|
+
"aria-label": t("courses.editor.videoUrlLabel") || "Video URL"
|
|
860
|
+
}
|
|
861
|
+
)
|
|
862
|
+
] }),
|
|
863
|
+
/* @__PURE__ */ jsxs("div", { className: "max-w-[160px] space-y-1.5", children: [
|
|
864
|
+
/* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t("courses.editor.lessonDuration") || "Duration (min)" }),
|
|
865
|
+
/* @__PURE__ */ jsx(
|
|
866
|
+
Input,
|
|
867
|
+
{
|
|
868
|
+
type: "number",
|
|
869
|
+
min: 0,
|
|
870
|
+
inputMode: "numeric",
|
|
871
|
+
defaultValue: String(Math.round(lesson.durationSec / 60)),
|
|
872
|
+
onBlur: (e) => {
|
|
873
|
+
const s = (Number(e.target.value) || 0) * 60;
|
|
874
|
+
setDurationSec(s);
|
|
875
|
+
onPatchLesson(lesson.id, { durationSec: s });
|
|
876
|
+
},
|
|
877
|
+
"aria-label": t("courses.editor.lessonDuration") || "Duration (min)"
|
|
878
|
+
}
|
|
879
|
+
)
|
|
880
|
+
] })
|
|
881
|
+
] })
|
|
882
|
+
]
|
|
883
|
+
}
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
function TitleField({ value, autoFocus, onAutoFocused, onCommit, placeholder, ariaLabel, className }) {
|
|
887
|
+
const ref = React.useRef(null);
|
|
888
|
+
React.useEffect(() => {
|
|
889
|
+
if (autoFocus && ref.current) {
|
|
890
|
+
ref.current.focus();
|
|
891
|
+
ref.current.select();
|
|
892
|
+
onAutoFocused?.();
|
|
893
|
+
}
|
|
894
|
+
}, [autoFocus]);
|
|
895
|
+
return /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx(
|
|
896
|
+
Input,
|
|
897
|
+
{
|
|
898
|
+
ref,
|
|
899
|
+
defaultValue: value,
|
|
900
|
+
placeholder,
|
|
901
|
+
"aria-label": ariaLabel,
|
|
902
|
+
onBlur: (e) => onCommit(e.target.value),
|
|
903
|
+
onKeyDown: (e) => {
|
|
904
|
+
if (e.key === "Enter") e.currentTarget.blur();
|
|
905
|
+
},
|
|
906
|
+
className: "h-auto rounded-md border-0 bg-transparent px-2 py-1.5 shadow-none transition-colors placeholder:text-muted-foreground/60 hover:bg-muted/60 focus:bg-background focus-visible:ring-offset-0 " + (className ?? "")
|
|
907
|
+
}
|
|
908
|
+
) });
|
|
909
|
+
}
|
|
910
|
+
function Field({ label, hint, children }) {
|
|
911
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
|
|
912
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between gap-2", children: [
|
|
913
|
+
/* @__PURE__ */ jsx("label", { className: "text-sm font-medium text-foreground", children: label }),
|
|
914
|
+
hint && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: hint })
|
|
915
|
+
] }),
|
|
916
|
+
children
|
|
917
|
+
] });
|
|
918
|
+
}
|
|
919
|
+
function AutosaveHint({ t }) {
|
|
920
|
+
return /* @__PURE__ */ jsxs("p", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [
|
|
921
|
+
/* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5 text-success" }),
|
|
922
|
+
t("courses.editor.autosaved") || "Changes are saved automatically."
|
|
923
|
+
] });
|
|
924
|
+
}
|
|
925
|
+
function EditorSkeleton() {
|
|
926
|
+
return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-4xl p-6 md:p-8", children: [
|
|
927
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "mb-4 h-4 w-28" }),
|
|
928
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "mb-6 h-8 w-64" }),
|
|
929
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "mb-6 h-10 w-full" }),
|
|
930
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
931
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-40 w-full rounded-xl" }),
|
|
932
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-40 w-full rounded-xl" })
|
|
933
|
+
] })
|
|
934
|
+
] });
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
export { CourseEditorPage };
|
|
938
|
+
//# sourceMappingURL=CourseEditorPage-GEBTVW7G.js.map
|
|
939
|
+
//# sourceMappingURL=CourseEditorPage-GEBTVW7G.js.map
|