@learnpack/learnpack 5.0.287 → 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-CQMtxRqZ.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.287",
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) {
@@ -116,29 +99,6 @@ function App() {
116
99
  }
117
100
  }
118
101
 
119
- const getGenerationMode = (): string => {
120
- const urlParams = new URLSearchParams(window.location.search);
121
- const configParams = urlParams.get('configParams');
122
-
123
- if (!configParams) {
124
- return 'next-three';
125
- }
126
-
127
- try {
128
- const decodedConfig = decodeURIComponent(configParams);
129
- const config = JSON.parse(decodedConfig);
130
-
131
- if (config?.page?.generation === 'auto') {
132
- return 'continue-with-all';
133
- }
134
-
135
- return 'next-three';
136
- } catch (error) {
137
- console.error('Error parsing configParams:', error);
138
- return 'next-three';
139
- }
140
- };
141
-
142
102
  const checkQueryParams = () => {
143
103
  const {
144
104
  description,
@@ -163,6 +123,7 @@ function App() {
163
123
  }
164
124
 
165
125
  if (description) {
126
+ console.log("description", description)
166
127
  setFormState({
167
128
  description: description,
168
129
  })
@@ -170,9 +131,12 @@ function App() {
170
131
  if (duration && !isNaN(parseInt(duration))) {
171
132
  if (["30", "60", "120"].includes(duration)) {
172
133
  const durationInt = parseInt(duration)
134
+ console.log("duration", durationInt)
173
135
  setFormState({
174
136
  duration: durationInt,
175
137
  })
138
+ } else {
139
+ console.log("Invalid duration received in params", duration)
176
140
  }
177
141
  }
178
142
 
@@ -238,13 +202,13 @@ function App() {
238
202
  }
239
203
  }
240
204
 
241
- if (auth.publicToken) {
242
- // const isPublicTokenValid = await isValidPublicToken(auth.publicToken)
243
- // if (isPublicTokenValid) {
244
- // }
245
- tokenToUse = auth.publicToken
246
- isAuthenticated = true
247
- 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
+ }
248
212
  }
249
213
  if (!isAuthenticated) {
250
214
  setShowTurnstileModal(true)
@@ -331,7 +295,7 @@ function App() {
331
295
  onFinish={(purpose) => {
332
296
  setFormState({
333
297
  purpose: purpose,
334
- currentStep: "duration",
298
+ currentStep: "verifyHuman",
335
299
  })
336
300
  }}
337
301
  />
@@ -350,7 +314,7 @@ function App() {
350
314
  onClick={() => {
351
315
  setFormState({
352
316
  duration: 30,
353
- currentStep: "hasContentIndex",
317
+ currentStep: "verifyHuman",
354
318
  })
355
319
  }}
356
320
  selected={formState.duration === 30}
@@ -361,7 +325,7 @@ function App() {
361
325
  onClick={() => {
362
326
  setFormState({
363
327
  duration: 60,
364
- currentStep: "hasContentIndex",
328
+ currentStep: "verifyHuman",
365
329
  })
366
330
  }}
367
331
  selected={formState.duration === 60}
@@ -372,7 +336,7 @@ function App() {
372
336
  onClick={() => {
373
337
  setFormState({
374
338
  duration: 120,
375
- currentStep: "hasContentIndex",
339
+ currentStep: "verifyHuman",
376
340
  })
377
341
  }}
378
342
  selected={formState.duration === 120}
@@ -381,20 +345,52 @@ function App() {
381
345
  ),
382
346
  },
383
347
  {
384
- title: t("stepWizard.hasContentIndex"),
385
- slug: "hasContentIndex",
348
+ title: t("stepWizard.verifyHuman"),
349
+ slug: "verifyHuman",
386
350
  isCompleted: false,
351
+ required: true,
387
352
  content: (
388
- <>
389
- {!auth.publicToken && (
390
- <PassphraseValidator onSuccess={(res) => {
391
- 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)
392
363
  setAuth({
393
364
  ...auth,
394
- publicToken: res.public_access_token,
365
+ publicToken: jwtToken,
366
+ })
367
+ setFormState({
368
+ currentStep: "hasContentIndex",
395
369
  })
396
- }} />
397
- )}
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
+ <>
398
394
  <div className="flex flex-col md:flex-row gap-2 justify-center">
399
395
  <SelectableCard
400
396
  title={t("stepWizard.hasContentIndexCard.no")}
@@ -409,7 +405,7 @@ function App() {
409
405
  isCompleted: true,
410
406
  })
411
407
  }}
412
- // selected={formState.hasContentIndex === false}
408
+ // selected={formState.hasContentIndex === false}
413
409
  />
414
410
  <SelectableCard
415
411
  title={t("stepWizard.hasContentIndexCard.yes")}
@@ -420,7 +416,7 @@ function App() {
420
416
  variables: [...formState.variables, "contentIndex"],
421
417
  })
422
418
  }}
423
- // selected={formState.hasContentIndex === true}
419
+ // selected={formState.hasContentIndex === true}
424
420
  />
425
421
  </div>
426
422
  </>
@@ -489,11 +485,9 @@ function App() {
489
485
 
490
486
  push({
491
487
  lessons,
492
- generationMode: getGenerationMode() as "next-three" | "continue-with-all",
493
488
  courseInfo: {
494
489
  ...formState,
495
490
  title: res.parsed.title,
496
-
497
491
  slug: slugify(fixTitleLength(res.parsed.title)),
498
492
  description: res.parsed.description,
499
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")