@learnpack/learnpack 5.0.288 → 5.0.290

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-7zTdUX04.js"></script>
14
14
  <link rel="stylesheet" crossorigin href="/creator/assets/index-C39zeF3W.css">
15
15
  </head>
16
16
  <body>
@@ -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.290",
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
 
@@ -698,6 +698,7 @@ export default class ServeCommand extends SessionCommand {
698
698
  }
699
699
  )
700
700
 
701
+ // TODO: Check if this command is being used
701
702
  app.post("/actions/generate-image/:courseSlug", async (req, res) => {
702
703
  const rigoToken = req.header("x-rigo-token")
703
704
  const { courseSlug } = req.params
@@ -1039,8 +1040,6 @@ export default class ServeCommand extends SessionCommand {
1039
1040
  const courseSlug = req.query.slug
1040
1041
  const lang = req.query.lang || "en"
1041
1042
 
1042
- console.log("LANG", lang)
1043
-
1044
1043
  if (!courseSlug) {
1045
1044
  return res.status(400).json({ error: "Missing courseSlug" })
1046
1045
  }
@@ -1574,53 +1573,6 @@ export default class ServeCommand extends SessionCommand {
1574
1573
  }
1575
1574
  })
1576
1575
 
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
1576
  app.get(
1625
1577
  "/courses/:courseSlug/exercises/:exerciseSlug/",
1626
1578
  async (req, res) => {
@@ -2048,7 +2000,8 @@ export default class ServeCommand extends SessionCommand {
2048
2000
  let outputPath: string
2049
2001
  let filename: string
2050
2002
 
2051
- if (format === "scorm") {
2003
+ switch (format) {
2004
+ case "scorm": {
2052
2005
  outputPath = await exportToScorm({
2053
2006
  courseSlug: course_slug,
2054
2007
  format: "scorm",
@@ -2056,7 +2009,23 @@ export default class ServeCommand extends SessionCommand {
2056
2009
  outDir: path.join(__dirname, "../output/directory"),
2057
2010
  })
2058
2011
  filename = `${course_slug}-scorm.zip`
2059
- } else if (format === "epub") {
2012
+
2013
+ break
2014
+ }
2015
+
2016
+ case "zip": {
2017
+ outputPath = await exportToZip({
2018
+ courseSlug: course_slug,
2019
+ format: "zip",
2020
+ bucket,
2021
+ outDir: path.join(__dirname, "../output/directory"),
2022
+ })
2023
+ filename = `${course_slug}.zip`
2024
+
2025
+ break
2026
+ }
2027
+
2028
+ case "epub": {
2060
2029
  console.log("EPUB export", metadata)
2061
2030
  // Validate required metadata for EPUB
2062
2031
  if (
@@ -2085,11 +2054,16 @@ export default class ServeCommand extends SessionCommand {
2085
2054
  metadata
2086
2055
  )
2087
2056
  filename = `${course_slug}.epub`
2088
- } else {
2057
+
2058
+ break
2059
+ }
2060
+
2061
+ default: {
2089
2062
  return res.status(400).json({
2090
- error: "Invalid format. Supported formats: scorm, epub",
2063
+ error: "Invalid format. Supported formats: scorm, epub, zip",
2091
2064
  })
2092
2065
  }
2066
+ }
2093
2067
 
2094
2068
  // Send the file and clean up
2095
2069
  res.download(outputPath, filename, err => {
@@ -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) {
@@ -140,6 +123,7 @@ function App() {
140
123
  }
141
124
 
142
125
  if (description) {
126
+ console.log("description", description)
143
127
  setFormState({
144
128
  description: description,
145
129
  })
@@ -147,9 +131,12 @@ function App() {
147
131
  if (duration && !isNaN(parseInt(duration))) {
148
132
  if (["30", "60", "120"].includes(duration)) {
149
133
  const durationInt = parseInt(duration)
134
+ console.log("duration", durationInt)
150
135
  setFormState({
151
136
  duration: durationInt,
152
137
  })
138
+ } else {
139
+ console.log("Invalid duration received in params", duration)
153
140
  }
154
141
  }
155
142
 
@@ -215,13 +202,13 @@ function App() {
215
202
  }
216
203
  }
217
204
 
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"
205
+ if (auth.publicToken && !isAuthenticated) {
206
+ const isPublicTokenValid = await isValidPublicToken(auth.publicToken)
207
+ if (isPublicTokenValid) {
208
+ tokenToUse = auth.publicToken
209
+ isAuthenticated = true
210
+ tokenType = "public"
211
+ }
225
212
  }
226
213
  if (!isAuthenticated) {
227
214
  setShowTurnstileModal(true)
@@ -308,7 +295,7 @@ function App() {
308
295
  onFinish={(purpose) => {
309
296
  setFormState({
310
297
  purpose: purpose,
311
- currentStep: "duration",
298
+ currentStep: "verifyHuman",
312
299
  })
313
300
  }}
314
301
  />
@@ -327,7 +314,7 @@ function App() {
327
314
  onClick={() => {
328
315
  setFormState({
329
316
  duration: 30,
330
- currentStep: "hasContentIndex",
317
+ currentStep: "verifyHuman",
331
318
  })
332
319
  }}
333
320
  selected={formState.duration === 30}
@@ -338,7 +325,7 @@ function App() {
338
325
  onClick={() => {
339
326
  setFormState({
340
327
  duration: 60,
341
- currentStep: "hasContentIndex",
328
+ currentStep: "verifyHuman",
342
329
  })
343
330
  }}
344
331
  selected={formState.duration === 60}
@@ -349,7 +336,7 @@ function App() {
349
336
  onClick={() => {
350
337
  setFormState({
351
338
  duration: 120,
352
- currentStep: "hasContentIndex",
339
+ currentStep: "verifyHuman",
353
340
  })
354
341
  }}
355
342
  selected={formState.duration === 120}
@@ -358,20 +345,52 @@ function App() {
358
345
  ),
359
346
  },
360
347
  {
361
- title: t("stepWizard.hasContentIndex"),
362
- slug: "hasContentIndex",
348
+ title: t("stepWizard.verifyHuman"),
349
+ slug: "verifyHuman",
363
350
  isCompleted: false,
351
+ required: true,
364
352
  content: (
365
- <>
366
- {!auth.publicToken && (
367
- <PassphraseValidator onSuccess={(res) => {
368
- console.log("RES FROM RIGO", res.public_access_token)
353
+ <TurnstileChallenge
354
+ siteKey={
355
+ DEV_MODE ? "0x4AAAAAABeKMBYYinMU4Ib0" : "0x4AAAAAABeZ9tjEevGBsJFU"
356
+ }
357
+ onSuccess={async (token) => {
358
+ const { human, message, token: jwtToken } = await isHuman(token)
359
+ if (human) {
360
+ toast.success(t("stepWizard.humanSuccess"))
361
+
362
+ console.log("JWT TOKEN received", jwtToken)
369
363
  setAuth({
370
364
  ...auth,
371
- publicToken: res.public_access_token,
365
+ publicToken: jwtToken,
366
+ })
367
+ setFormState({
368
+ currentStep: "hasContentIndex",
372
369
  })
373
- }} />
374
- )}
370
+ } else {
371
+ toast.error(message)
372
+ setFormState({
373
+ currentStep: "duration",
374
+ })
375
+ }
376
+ }}
377
+ onError={() => {
378
+ toast.error(t("turnstileModal.error"), {
379
+ duration: 10000,
380
+ })
381
+ setFormState({
382
+ currentStep: "duration",
383
+ })
384
+ }}
385
+ />
386
+ ),
387
+ },
388
+ {
389
+ title: t("stepWizard.hasContentIndex"),
390
+ slug: "hasContentIndex",
391
+ isCompleted: false,
392
+ content: (
393
+ <>
375
394
  <div className="flex flex-col md:flex-row gap-2 justify-center">
376
395
  <SelectableCard
377
396
  title={t("stepWizard.hasContentIndexCard.no")}
@@ -386,7 +405,7 @@ function App() {
386
405
  isCompleted: true,
387
406
  })
388
407
  }}
389
- // selected={formState.hasContentIndex === false}
408
+ // selected={formState.hasContentIndex === false}
390
409
  />
391
410
  <SelectableCard
392
411
  title={t("stepWizard.hasContentIndexCard.yes")}
@@ -397,7 +416,7 @@ function App() {
397
416
  variables: [...formState.variables, "contentIndex"],
398
417
  })
399
418
  }}
400
- // selected={formState.hasContentIndex === true}
419
+ // selected={formState.hasContentIndex === true}
401
420
  />
402
421
  </div>
403
422
  </>
@@ -466,11 +485,9 @@ function App() {
466
485
 
467
486
  push({
468
487
  lessons,
469
- generationMode: "continue-with-all",
470
488
  courseInfo: {
471
489
  ...formState,
472
490
  title: res.parsed.title,
473
-
474
491
  slug: slugify(fixTitleLength(res.parsed.title)),
475
492
  description: res.parsed.description,
476
493
  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")