@learnpack/learnpack 5.0.288 → 5.0.291

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.
@@ -10,7 +10,7 @@
10
10
  />
11
11
 
12
12
  <title>Learnpack Creator: Craft tutorials in seconds!</title>
13
- <script type="module" crossorigin src="/creator/assets/index-BjO3hUuz.js"></script>
13
+ <script type="module" crossorigin src="/creator/assets/index-wLKEQIG6.js"></script>
14
14
  <link rel="stylesheet" crossorigin href="/creator/assets/index-C39zeF3W.css">
15
15
  </head>
16
16
  <body>
@@ -1,6 +1,23 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildConfig = buildConfig;
4
+ function naturalCompare(a, b) {
5
+ // Split by dots and hyphens, compare numbers as numbers
6
+ const regex = /(\d+|\D+)/g;
7
+ const ax = a.match(regex);
8
+ const bx = b.match(regex);
9
+ for (let i = 0; i < Math.max(ax.length, bx.length); i++) {
10
+ const an = parseInt(ax[i], 10);
11
+ const bn = parseInt(bx[i], 10);
12
+ if (!isNaN(an) && !isNaN(bn)) {
13
+ if (an !== bn)
14
+ return an - bn;
15
+ }
16
+ else if (ax[i] !== bx[i])
17
+ return (ax[i] || "").localeCompare(bx[i] || "");
18
+ }
19
+ return 0;
20
+ }
4
21
  /**
5
22
  * Crea la configuración y lista de ejercicios para un curso.
6
23
  *
@@ -45,7 +62,9 @@ async function buildConfig(bucket, courseSlug) {
45
62
  });
46
63
  }
47
64
  }
48
- const exercises = Object.values(map).map((ex, i) => (Object.assign(Object.assign({}, ex), { position: i })));
65
+ const exercises = Object.values(map)
66
+ .sort((a, b) => naturalCompare(a.slug, b.slug))
67
+ .map((ex, i) => (Object.assign(Object.assign({}, ex), { position: i })));
49
68
  return {
50
69
  config: Object.assign({}, learnJson),
51
70
  exercises,
@@ -1,3 +1,4 @@
1
1
  export { exportToScorm } from "./scorm";
2
2
  export { exportToEpub } from "./epub";
3
+ export { exportToZip } from "./zip";
3
4
  export { ExportFormat, ExportOptions, EpubMetadata } from "./types";
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.exportToEpub = exports.exportToScorm = void 0;
3
+ exports.exportToZip = exports.exportToEpub = exports.exportToScorm = void 0;
4
4
  var scorm_1 = require("./scorm");
5
5
  Object.defineProperty(exports, "exportToScorm", { enumerable: true, get: function () { return scorm_1.exportToScorm; } });
6
6
  var epub_1 = require("./epub");
7
7
  Object.defineProperty(exports, "exportToEpub", { enumerable: true, get: function () { return epub_1.exportToEpub; } });
8
+ var zip_1 = require("./zip");
9
+ Object.defineProperty(exports, "exportToZip", { enumerable: true, get: function () { return zip_1.exportToZip; } });
@@ -1,4 +1,4 @@
1
- export type ExportFormat = "scorm" | "epub";
1
+ export type ExportFormat = "scorm" | "epub" | "zip";
2
2
  export interface ExportOptions {
3
3
  courseSlug: string;
4
4
  format: ExportFormat;
@@ -0,0 +1,2 @@
1
+ import { ExportOptions } from "./types";
2
+ export declare function exportToZip(options: ExportOptions): Promise<string>;
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.exportToZip = exportToZip;
4
+ const path = require("path");
5
+ const fs = require("fs");
6
+ const archiver = require("archiver");
7
+ const mkdirp = require("mkdirp");
8
+ const rimraf = require("rimraf");
9
+ const uuid_1 = require("uuid");
10
+ const shared_1 = require("./shared");
11
+ async function exportToZip(options) {
12
+ const { courseSlug, bucket, outDir } = options;
13
+ // 1. Create temporary folder
14
+ const tmpName = (0, uuid_1.v4)();
15
+ const zipOutDir = path.join(outDir, tmpName);
16
+ rimraf.sync(zipOutDir);
17
+ mkdirp.sync(zipOutDir);
18
+ // 2. Download all course files from bucket to temporary directory
19
+ await (0, shared_1.downloadS3Folder)(bucket, `courses/${courseSlug}/`, zipOutDir);
20
+ // 3. Create ZIP file
21
+ const zipName = `${courseSlug}.zip`;
22
+ const zipPath = path.join(outDir, zipName);
23
+ // Remove existing zip file if it exists
24
+ if (fs.existsSync(zipPath)) {
25
+ fs.unlinkSync(zipPath);
26
+ }
27
+ const output = fs.createWriteStream(zipPath);
28
+ const archive = archiver("zip", { zlib: { level: 9 } });
29
+ return new Promise((resolve, reject) => {
30
+ output.on("close", () => {
31
+ console.log(`✅ ZIP export completed: ${archive.pointer()} total bytes`);
32
+ // Clean up temporary directory
33
+ rimraf.sync(zipOutDir);
34
+ resolve(zipPath);
35
+ });
36
+ archive.on("error", (err) => {
37
+ console.error("❌ ZIP creation error:", err);
38
+ rimraf.sync(zipOutDir);
39
+ reject(err);
40
+ });
41
+ archive.pipe(output);
42
+ // Add all files from the temporary directory to the ZIP
43
+ archive.directory(zipOutDir, false);
44
+ archive.finalize();
45
+ });
46
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@learnpack/learnpack",
3
3
  "description": "Seamlessly build, sell and/or take interactive & auto-graded tutorials, start learning now or build a new tutorial to your audience.",
4
- "version": "5.0.288",
4
+ "version": "5.0.291",
5
5
  "author": "Alejandro Sanchez @alesanchezr",
6
6
  "contributors": [
7
7
  {
@@ -56,7 +56,7 @@ import { checkReadability, slugify } from "../utils/creatorUtilities"
56
56
  import { checkAndFixSidebarPure } from "../utils/sidebarGenerator"
57
57
  import { handleAssetCreation } from "./publish"
58
58
  import { FormState, Lesson, Syllabus } from "../models/creator"
59
- import { exportToScorm, exportToEpub } from "../utils/export"
59
+ import { exportToScorm, exportToEpub, exportToZip } from "../utils/export"
60
60
 
61
61
  const frontMatter = require("front-matter")
62
62
 
@@ -211,9 +211,17 @@ const createMultiLangAsset = async (
211
211
  const indexReadme = await bucket.file(
212
212
  `courses/${courseSlug}/README${getReadmeExtension(lang)}`
213
213
  )
214
- // eslint-disable-next-line no-await-in-loop
215
- const [indexReadmeContent] = await indexReadme.download()
216
- const indexReadmeString = indexReadmeContent.toString()
214
+ let indexReadmeString = ""
215
+ try {
216
+ // eslint-disable-next-line no-await-in-loop
217
+ const [indexReadmeContent] = await indexReadme.download()
218
+ indexReadmeString = indexReadmeContent.toString()
219
+ } catch (error) {
220
+ console.error("Error downloading index readme", error)
221
+ // TODO: Trigger generation of the index readme
222
+ indexReadmeString = ""
223
+ }
224
+
217
225
  const b64IndexReadme = Buffer.from(indexReadmeString).toString("base64")
218
226
 
219
227
  // eslint-disable-next-line no-await-in-loop
@@ -698,6 +706,7 @@ export default class ServeCommand extends SessionCommand {
698
706
  }
699
707
  )
700
708
 
709
+ // TODO: Check if this command is being used
701
710
  app.post("/actions/generate-image/:courseSlug", async (req, res) => {
702
711
  const rigoToken = req.header("x-rigo-token")
703
712
  const { courseSlug } = req.params
@@ -1017,6 +1026,8 @@ export default class ServeCommand extends SessionCommand {
1017
1026
  try {
1018
1027
  console.log("GET CONFIG, COURSE SLUG", courseSlug)
1019
1028
  const { config, exercises } = await buildConfig(bucket, courseSlug)
1029
+ console.log("CONFIG", config)
1030
+ console.log("EXERCISES", exercises)
1020
1031
  res.set("X-Creator-Web", "true")
1021
1032
  res.set("Access-Control-Expose-Headers", "X-Creator-Web")
1022
1033
 
@@ -1039,8 +1050,6 @@ export default class ServeCommand extends SessionCommand {
1039
1050
  const courseSlug = req.query.slug
1040
1051
  const lang = req.query.lang || "en"
1041
1052
 
1042
- console.log("LANG", lang)
1043
-
1044
1053
  if (!courseSlug) {
1045
1054
  return res.status(400).json({ error: "Missing courseSlug" })
1046
1055
  }
@@ -1574,53 +1583,6 @@ export default class ServeCommand extends SessionCommand {
1574
1583
  }
1575
1584
  })
1576
1585
 
1577
- // app.post("/actions/continue-course/:courseSlug", async (req, res) => {
1578
- // console.log("POST /actions/continue-course/:courseSlug")
1579
- // const { courseSlug } = req.params
1580
-
1581
- // const { feedback }: { feedback: string } = req.body
1582
-
1583
- // const rigoToken = req.header("x-rigo-token")
1584
- // const bcToken = req.header("x-breathecode-token")
1585
- // if (!rigoToken || !bcToken) {
1586
- // return res.status(400).json({ error: "Missing tokens" })
1587
- // }
1588
-
1589
- // const syllabus = await bucket.file(
1590
- // `courses/${courseSlug}/.learn/initialSyllabus.json`
1591
- // )
1592
- // const [content] = await syllabus.download()
1593
- // const syllabusJson: Syllabus = JSON.parse(content.toString())
1594
- // const notGeneratedLessons = syllabusJson.lessons.filter(
1595
- // lesson => !lesson.generated
1596
- // )
1597
-
1598
- // const lastGeneratedLesson = findLast(
1599
- // syllabusJson.lessons,
1600
- // lesson => lesson.generated ?? false
1601
- // )
1602
-
1603
- // console.log("ABout to generate", notGeneratedLessons.length, "lessons")
1604
-
1605
- // const firstLessonToGenerate = notGeneratedLessons[0]
1606
-
1607
- // const completionId = await startExerciseGeneration(
1608
- // rigoToken,
1609
- // syllabusJson.lessons,
1610
- // syllabusJson.courseInfo,
1611
- // firstLessonToGenerate,
1612
- // courseSlug,
1613
- // syllabusJson.courseInfo.purpose,
1614
- // JSON.stringify(lastGeneratedLesson) +
1615
- // `\n\nThe user provided this feedback in relation to the course: ${feedback}`
1616
- // )
1617
-
1618
- // return res.json({
1619
- // message: "Course continued",
1620
- // slug: courseSlug,
1621
- // })
1622
- // })
1623
-
1624
1586
  app.get(
1625
1587
  "/courses/:courseSlug/exercises/:exerciseSlug/",
1626
1588
  async (req, res) => {
@@ -2048,47 +2010,72 @@ export default class ServeCommand extends SessionCommand {
2048
2010
  let outputPath: string
2049
2011
  let filename: string
2050
2012
 
2051
- if (format === "scorm") {
2052
- outputPath = await exportToScorm({
2053
- courseSlug: course_slug,
2054
- format: "scorm",
2055
- bucket,
2056
- outDir: path.join(__dirname, "../output/directory"),
2057
- })
2058
- filename = `${course_slug}-scorm.zip`
2059
- } else if (format === "epub") {
2060
- console.log("EPUB export", metadata)
2061
- // Validate required metadata for EPUB
2062
- if (
2063
- !metadata ||
2064
- !metadata.creator ||
2065
- !metadata.publisher ||
2066
- !metadata.title ||
2067
- !metadata.rights ||
2068
- !metadata.lang
2069
- ) {
2070
- console.log("Missing required metadata for EPUB export", metadata)
2071
- return res.status(400).json({
2072
- error: "Missing required metadata for EPUB export",
2073
- required: ["creator", "publisher", "title", "rights", "lang"],
2013
+ switch (format) {
2014
+ case "scorm": {
2015
+ outputPath = await exportToScorm({
2016
+ courseSlug: course_slug,
2017
+ format: "scorm",
2018
+ bucket,
2019
+ outDir: path.join(__dirname, "../output/directory"),
2074
2020
  })
2021
+ filename = `${course_slug}-scorm.zip`
2022
+
2023
+ break
2075
2024
  }
2076
2025
 
2077
- outputPath = await exportToEpub(
2078
- {
2026
+ case "zip": {
2027
+ outputPath = await exportToZip({
2079
2028
  courseSlug: course_slug,
2080
- format: "epub",
2029
+ format: "zip",
2081
2030
  bucket,
2082
2031
  outDir: path.join(__dirname, "../output/directory"),
2083
- language: language,
2084
- },
2085
- metadata
2086
- )
2087
- filename = `${course_slug}.epub`
2088
- } else {
2089
- return res.status(400).json({
2090
- error: "Invalid format. Supported formats: scorm, epub",
2091
- })
2032
+ })
2033
+ filename = `${course_slug}.zip`
2034
+
2035
+ break
2036
+ }
2037
+
2038
+ case "epub": {
2039
+ console.log("EPUB export", metadata)
2040
+ // Validate required metadata for EPUB
2041
+ if (
2042
+ !metadata ||
2043
+ !metadata.creator ||
2044
+ !metadata.publisher ||
2045
+ !metadata.title ||
2046
+ !metadata.rights ||
2047
+ !metadata.lang
2048
+ ) {
2049
+ console.log(
2050
+ "Missing required metadata for EPUB export",
2051
+ metadata
2052
+ )
2053
+ return res.status(400).json({
2054
+ error: "Missing required metadata for EPUB export",
2055
+ required: ["creator", "publisher", "title", "rights", "lang"],
2056
+ })
2057
+ }
2058
+
2059
+ outputPath = await exportToEpub(
2060
+ {
2061
+ courseSlug: course_slug,
2062
+ format: "epub",
2063
+ bucket,
2064
+ outDir: path.join(__dirname, "../output/directory"),
2065
+ language: language,
2066
+ },
2067
+ metadata
2068
+ )
2069
+ filename = `${course_slug}.epub`
2070
+
2071
+ break
2072
+ }
2073
+
2074
+ default: {
2075
+ return res.status(400).json({
2076
+ error: "Invalid format. Supported formats: scorm, epub, zip",
2077
+ })
2078
+ }
2092
2079
  }
2093
2080
 
2094
2081
  // Send the file and clean up
@@ -6,7 +6,7 @@ import { useNavigate } from "react-router"
6
6
  import { useShallow } from "zustand/react/shallow"
7
7
  import useStore, { TDifficulty } from "./utils/store"
8
8
 
9
- import { publicInteractiveCreation } from "./utils/rigo"
9
+ import { publicInteractiveCreation, isHuman } from "./utils/rigo"
10
10
  import {
11
11
  checkParams,
12
12
  isValidRigoToken,
@@ -21,8 +21,8 @@ import {
21
21
  import { Uploader } from "./components/Uploader"
22
22
  import toast from "react-hot-toast"
23
23
  import { ParamsChecker } from "./components/ParamsChecker"
24
- import { RIGO_FLOAT_GIF } from "./utils/constants"
25
- // import TurnstileChallenge from "./components/TurnstileChallenge"
24
+ import { DEV_MODE, RIGO_FLOAT_GIF } from "./utils/constants"
25
+ import TurnstileChallenge from "./components/TurnstileChallenge"
26
26
  // import TurnstileChallenge from "./components/TurnstileChallenge"
27
27
  import ResumeCourseModal from "./components/ResumeCourseModal"
28
28
  import { possiblePurposes, PurposeSelector } from "./components/PurposeSelector"
@@ -31,7 +31,6 @@ import NotificationListener from "./components/NotificationListener"
31
31
  import { slugify } from "./utils/creatorUtils"
32
32
  import TurnstileModal from "./components/TurnstileModal"
33
33
  import { TMessage } from "./components/Message"
34
- import PassphraseValidator from "./components/PassphraseValidator"
35
34
 
36
35
  function App() {
37
36
  const navigate = useNavigate()
@@ -86,22 +85,6 @@ function App() {
86
85
  checkTechs()
87
86
  }, [])
88
87
 
89
- const tokenVerification = async () => {
90
- const isValid = await isValidPublicToken(auth.publicToken)
91
- if (!isValid) {
92
- setAuth({
93
- ...auth,
94
- publicToken: ""
95
- })
96
- }
97
- }
98
-
99
- useEffect(() => {
100
- if (auth.publicToken) {
101
- tokenVerification()
102
- }
103
- }, [auth])
104
-
105
88
  const verifyToken = async () => {
106
89
  const { token } = checkParams(["token"])
107
90
  if (token) {
@@ -147,9 +130,12 @@ function App() {
147
130
  if (duration && !isNaN(parseInt(duration))) {
148
131
  if (["30", "60", "120"].includes(duration)) {
149
132
  const durationInt = parseInt(duration)
133
+ console.log("duration", durationInt)
150
134
  setFormState({
151
135
  duration: durationInt,
152
136
  })
137
+ } else {
138
+ console.log("Invalid duration received in params", duration)
153
139
  }
154
140
  }
155
141
 
@@ -215,13 +201,13 @@ function App() {
215
201
  }
216
202
  }
217
203
 
218
- if (auth.publicToken) {
219
- // const isPublicTokenValid = await isValidPublicToken(auth.publicToken)
220
- // if (isPublicTokenValid) {
221
- // }
222
- tokenToUse = auth.publicToken
223
- isAuthenticated = true
224
- tokenType = "public"
204
+ if (auth.publicToken && !isAuthenticated) {
205
+ const isPublicTokenValid = await isValidPublicToken(auth.publicToken)
206
+ if (isPublicTokenValid) {
207
+ tokenToUse = auth.publicToken
208
+ isAuthenticated = true
209
+ tokenType = "public"
210
+ }
225
211
  }
226
212
  if (!isAuthenticated) {
227
213
  setShowTurnstileModal(true)
@@ -327,7 +313,7 @@ function App() {
327
313
  onClick={() => {
328
314
  setFormState({
329
315
  duration: 30,
330
- currentStep: "hasContentIndex",
316
+ currentStep: "verifyHuman",
331
317
  })
332
318
  }}
333
319
  selected={formState.duration === 30}
@@ -338,7 +324,7 @@ function App() {
338
324
  onClick={() => {
339
325
  setFormState({
340
326
  duration: 60,
341
- currentStep: "hasContentIndex",
327
+ currentStep: "verifyHuman",
342
328
  })
343
329
  }}
344
330
  selected={formState.duration === 60}
@@ -349,7 +335,7 @@ function App() {
349
335
  onClick={() => {
350
336
  setFormState({
351
337
  duration: 120,
352
- currentStep: "hasContentIndex",
338
+ currentStep: "verifyHuman",
353
339
  })
354
340
  }}
355
341
  selected={formState.duration === 120}
@@ -358,20 +344,52 @@ function App() {
358
344
  ),
359
345
  },
360
346
  {
361
- title: t("stepWizard.hasContentIndex"),
362
- slug: "hasContentIndex",
347
+ title: t("stepWizard.verifyHuman"),
348
+ slug: "verifyHuman",
363
349
  isCompleted: false,
350
+ required: true,
364
351
  content: (
365
- <>
366
- {!auth.publicToken && (
367
- <PassphraseValidator onSuccess={(res) => {
368
- console.log("RES FROM RIGO", res.public_access_token)
352
+ <TurnstileChallenge
353
+ siteKey={
354
+ DEV_MODE ? "0x4AAAAAABeKMBYYinMU4Ib0" : "0x4AAAAAABeZ9tjEevGBsJFU"
355
+ }
356
+ onSuccess={async (token) => {
357
+ const { human, message, token: jwtToken } = await isHuman(token)
358
+ if (human) {
359
+ toast.success(t("stepWizard.humanSuccess"))
360
+
361
+ console.log("JWT TOKEN received", jwtToken)
369
362
  setAuth({
370
363
  ...auth,
371
- publicToken: res.public_access_token,
364
+ publicToken: jwtToken,
372
365
  })
373
- }} />
374
- )}
366
+ setFormState({
367
+ currentStep: "hasContentIndex",
368
+ })
369
+ } else {
370
+ toast.error(message)
371
+ setFormState({
372
+ currentStep: "duration",
373
+ })
374
+ }
375
+ }}
376
+ onError={() => {
377
+ toast.error(t("turnstileModal.error"), {
378
+ duration: 10000,
379
+ })
380
+ setFormState({
381
+ currentStep: "duration",
382
+ })
383
+ }}
384
+ />
385
+ ),
386
+ },
387
+ {
388
+ title: t("stepWizard.hasContentIndex"),
389
+ slug: "hasContentIndex",
390
+ isCompleted: false,
391
+ content: (
392
+ <>
375
393
  <div className="flex flex-col md:flex-row gap-2 justify-center">
376
394
  <SelectableCard
377
395
  title={t("stepWizard.hasContentIndexCard.no")}
@@ -386,7 +404,7 @@ function App() {
386
404
  isCompleted: true,
387
405
  })
388
406
  }}
389
- // selected={formState.hasContentIndex === false}
407
+ // selected={formState.hasContentIndex === false}
390
408
  />
391
409
  <SelectableCard
392
410
  title={t("stepWizard.hasContentIndexCard.yes")}
@@ -397,7 +415,7 @@ function App() {
397
415
  variables: [...formState.variables, "contentIndex"],
398
416
  })
399
417
  }}
400
- // selected={formState.hasContentIndex === true}
418
+ // selected={formState.hasContentIndex === true}
401
419
  />
402
420
  </div>
403
421
  </>
@@ -466,11 +484,9 @@ function App() {
466
484
 
467
485
  push({
468
486
  lessons,
469
- generationMode: "continue-with-all",
470
487
  courseInfo: {
471
488
  ...formState,
472
489
  title: res.parsed.title,
473
-
474
490
  slug: slugify(fixTitleLength(res.parsed.title)),
475
491
  description: res.parsed.description,
476
492
  language:
@@ -14,7 +14,6 @@ import {
14
14
  useConsumableCall,
15
15
  isValidRigoToken,
16
16
  isValidPublicToken,
17
- getMyPackages,
18
17
  } from "../../utils/lib"
19
18
 
20
19
  import Loader from "../Loader"
@@ -75,7 +74,7 @@ const SyllabusEditor: React.FC = () => {
75
74
  }, [syllabus, navigate])
76
75
 
77
76
  useEffect(() => {
78
- ; (async () => {
77
+ ;(async () => {
79
78
  const { token } = checkParams(["token"])
80
79
  if (token) {
81
80
  const user = await loginWithToken(token)
@@ -93,25 +92,6 @@ const SyllabusEditor: React.FC = () => {
93
92
  checkSlug()
94
93
  }, [])
95
94
 
96
-
97
- const checkMyPackages = async () => {
98
- if (auth.rigoToken) {
99
- const packages = await getMyPackages(auth.rigoToken)
100
- if (Array.isArray(packages) && packages.length === 0) {
101
- push({
102
- ...syllabus,
103
- generationMode: "continue-with-all",
104
- })
105
- }
106
- }
107
- }
108
-
109
- useEffect(() => {
110
- if (auth.rigoToken) {
111
- checkMyPackages()
112
- }
113
- }, [auth])
114
-
115
95
  const checkSlug = async () => {
116
96
  if (!syllabus.courseInfo.title) {
117
97
  toast.error("Please provide a title for the course")