@learnpack/learnpack 5.0.144 → 5.0.148

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,8 +10,8 @@
10
10
  />
11
11
 
12
12
  <title>Learnpack Creator: Craft tutorials in seconds!</title>
13
- <script type="module" crossorigin src="/creator/assets/index-CzMCewx6.js"></script>
14
- <link rel="stylesheet" crossorigin href="/creator/assets/index-DSOj0E0h.css">
13
+ <script type="module" crossorigin src="/creator/assets/index-ETBXfIew.js"></script>
14
+ <link rel="stylesheet" crossorigin href="/creator/assets/index-DUPYM87B.css">
15
15
  </head>
16
16
  <body>
17
17
  <div id="root"></div>
@@ -9,4 +9,8 @@ export type TSidebar = {
9
9
  export declare const generateSidebar: (exercises: IExercise[], learnPath: string) => TSidebar;
10
10
  export declare const addExerciseToSidebar: (exerciseSlug: string, targetLanguage: string, translatedSlug: string, learnPath: string) => TSidebar;
11
11
  export declare const checkAndFixSidebar: (configObj: IConfigObj, autoFix?: boolean) => Promise<boolean>;
12
+ export declare const checkAndFixSidebarPure: (sidebarJson: Record<string, Record<string, string>>, exercises: any[], rigoToken: string, autoFix?: boolean) => Promise<{
13
+ valid: boolean;
14
+ fixedSidebar?: Record<string, Record<string, string>>;
15
+ }>;
12
16
  export {};
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.checkAndFixSidebar = exports.addExerciseToSidebar = exports.generateSidebar = void 0;
3
+ exports.checkAndFixSidebarPure = exports.checkAndFixSidebar = exports.addExerciseToSidebar = exports.generateSidebar = void 0;
4
4
  const path = require("path");
5
5
  const console_1 = require("./console");
6
6
  const fs = require("fs");
@@ -75,3 +75,49 @@ const checkAndFixSidebar = async (configObj, autoFix = false) => {
75
75
  return false;
76
76
  };
77
77
  exports.checkAndFixSidebar = checkAndFixSidebar;
78
+ const checkAndFixSidebarPure = async (sidebarJson, exercises, rigoToken, autoFix = true) => {
79
+ const exerciseTranslations = new Set();
80
+ for (const e of exercises) {
81
+ for (const lang of Object.keys(e.translations || {})) {
82
+ exerciseTranslations.add(lang);
83
+ }
84
+ }
85
+ let hasErrors = false;
86
+ const fixedSidebar = Object.assign({}, sidebarJson);
87
+ for (const exercise of exercises) {
88
+ const slug = exercise.slug;
89
+ const existingEntry = fixedSidebar[slug];
90
+ if (!existingEntry) {
91
+ hasErrors = true;
92
+ fixedSidebar[slug] = { us: exercise.slug };
93
+ continue;
94
+ }
95
+ for (const lang of exerciseTranslations) {
96
+ if (!Object.prototype.hasOwnProperty.call(existingEntry, lang)) {
97
+ hasErrors = true;
98
+ }
99
+ }
100
+ }
101
+ if (hasErrors && autoFix) {
102
+ if (!rigoToken) {
103
+ console_1.default.error("No Rigobot token provided!");
104
+ return { valid: false };
105
+ }
106
+ console_1.default.warning("Filling sidebar JSON with missing translations or exercises");
107
+ const response = await (0, rigoActions_1.fillSidebarJSON)(rigoToken, {
108
+ needed_translations: JSON.stringify([...exerciseTranslations]),
109
+ sidebar_json: JSON.stringify(fixedSidebar),
110
+ });
111
+ const newSidebarJson = JSON.parse(response.parsed.new_sidebar_file);
112
+ console_1.default.info("Sidebar JSON was fixed");
113
+ return {
114
+ valid: true,
115
+ fixedSidebar: newSidebarJson,
116
+ };
117
+ }
118
+ return {
119
+ valid: !hasErrors,
120
+ fixedSidebar: hasErrors ? fixedSidebar : undefined,
121
+ };
122
+ };
123
+ exports.checkAndFixSidebarPure = checkAndFixSidebarPure;
@@ -1 +1 @@
1
- {"version":"5.0.144","commands":{"audit":{"id":"audit","description":"learnpack audit is the command in charge of creating an auditory of the repository\n...\nlearnpack audit checks for the following information in a repository:\n 1. The configuration object has slug, repository and description. (Error)\n 2. The command learnpack clean has been run. (Error)\n 3. If a markdown or test file doesn't have any content. (Error)\n 4. The links are accessing to valid servers. (Error)\n 5. The relative images are working (If they have the shortest path to the image or if the images exists in the assets). (Error)\n 6. The external images are working (If they are pointing to a valid server). (Error)\n 7. The exercises directory names are valid. (Error)\n 8. If an exercise doesn't have a README file. (Error)\n 9. The exercises array (Of the config file) has content. (Error)\n 10. The exercses have the same translations. (Warning)\n 11. The .gitignore file exists. (Warning)\n 12. If there is a file within the exercises folder but not inside of any particular exercise's folder. (Warning)\n","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"strict":{"name":"strict","type":"boolean","char":"s","description":"strict mode","allowNo":false}},"args":[]},"breakToken":{"id":"breakToken","description":"Break the token","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"grading":{"name":"grading","type":"boolean","char":"h","description":"show CLI help","allowNo":false}},"args":[]},"clean":{"id":"clean","description":"Clean the configuration object\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[]},"download":{"id":"download","description":"Describe the command here\n...\nExtra documentation goes here\n","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"init":{"id":"init","description":"Create a new learning package: Book, Tutorial or Exercise","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"grading":{"name":"grading","type":"boolean","char":"h","description":"show CLI help","allowNo":false}},"args":[]},"login":{"id":"login","description":"Describe the command here\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"logout":{"id":"logout","description":"Describe the command here\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"publish":{"id":"publish","description":"Builds the project by copying necessary files and directories into a zip file","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"strict":{"name":"strict","type":"boolean","char":"s","description":"strict mode","allowNo":false},"help":{"name":"help","type":"boolean","char":"h","description":"show CLI help","allowNo":false}},"args":[]},"serve":{"id":"serve","description":"Runs a small server to build tutorials","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"port":{"name":"port","type":"option","char":"p","description":"server port"},"host":{"name":"host","type":"option","char":"h","description":"server host"},"debug":{"name":"debug","type":"boolean","char":"d","description":"debugger mode for more verbage","allowNo":false}},"args":[]},"start":{"id":"start","description":"Runs a small server with all the exercise instructions","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"port":{"name":"port","type":"option","char":"p","description":"server port"},"host":{"name":"host","type":"option","char":"h","description":"server host"},"disableGrading":{"name":"disableGrading","type":"boolean","char":"D","description":"disble grading functionality","allowNo":false},"watch":{"name":"watch","type":"boolean","char":"w","description":"Watch for file changes","allowNo":false},"editor":{"name":"editor","type":"option","char":"e","description":"[preview, extension]","options":["extension","preview"]},"version":{"name":"version","type":"option","char":"v","description":"E.g: 1.0.1"},"grading":{"name":"grading","type":"option","char":"g","description":"[isolated, incremental]","options":["isolated","incremental"]},"debug":{"name":"debug","type":"boolean","char":"d","description":"debugger mode for more verbage","allowNo":false}},"args":[]},"test":{"id":"test","description":"Test exercises","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false}},"args":[{"name":"exerciseSlug","description":"The name of the exercise to test","required":false,"hidden":false}]},"translate":{"id":"translate","description":"List all the lessons, the user is able of select many of them to translate to the given languages","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false}},"args":[]}}}
1
+ {"version":"5.0.148","commands":{"audit":{"id":"audit","description":"learnpack audit is the command in charge of creating an auditory of the repository\n...\nlearnpack audit checks for the following information in a repository:\n 1. The configuration object has slug, repository and description. (Error)\n 2. The command learnpack clean has been run. (Error)\n 3. If a markdown or test file doesn't have any content. (Error)\n 4. The links are accessing to valid servers. (Error)\n 5. The relative images are working (If they have the shortest path to the image or if the images exists in the assets). (Error)\n 6. The external images are working (If they are pointing to a valid server). (Error)\n 7. The exercises directory names are valid. (Error)\n 8. If an exercise doesn't have a README file. (Error)\n 9. The exercises array (Of the config file) has content. (Error)\n 10. The exercses have the same translations. (Warning)\n 11. The .gitignore file exists. (Warning)\n 12. If there is a file within the exercises folder but not inside of any particular exercise's folder. (Warning)\n","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"strict":{"name":"strict","type":"boolean","char":"s","description":"strict mode","allowNo":false}},"args":[]},"breakToken":{"id":"breakToken","description":"Break the token","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"grading":{"name":"grading","type":"boolean","char":"h","description":"show CLI help","allowNo":false}},"args":[]},"clean":{"id":"clean","description":"Clean the configuration object\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[]},"download":{"id":"download","description":"Describe the command here\n...\nExtra documentation goes here\n","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"init":{"id":"init","description":"Create a new learning package: Book, Tutorial or Exercise","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"grading":{"name":"grading","type":"boolean","char":"h","description":"show CLI help","allowNo":false}},"args":[]},"login":{"id":"login","description":"Describe the command here\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"logout":{"id":"logout","description":"Describe the command here\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"publish":{"id":"publish","description":"Builds the project by copying necessary files and directories into a zip file","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"strict":{"name":"strict","type":"boolean","char":"s","description":"strict mode","allowNo":false},"help":{"name":"help","type":"boolean","char":"h","description":"show CLI help","allowNo":false}},"args":[]},"serve":{"id":"serve","description":"Runs a small server to build tutorials","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"port":{"name":"port","type":"option","char":"p","description":"server port"},"host":{"name":"host","type":"option","char":"h","description":"server host"},"debug":{"name":"debug","type":"boolean","char":"d","description":"debugger mode for more verbage","allowNo":false}},"args":[]},"start":{"id":"start","description":"Runs a small server with all the exercise instructions","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"port":{"name":"port","type":"option","char":"p","description":"server port"},"host":{"name":"host","type":"option","char":"h","description":"server host"},"disableGrading":{"name":"disableGrading","type":"boolean","char":"D","description":"disble grading functionality","allowNo":false},"watch":{"name":"watch","type":"boolean","char":"w","description":"Watch for file changes","allowNo":false},"editor":{"name":"editor","type":"option","char":"e","description":"[preview, extension]","options":["extension","preview"]},"version":{"name":"version","type":"option","char":"v","description":"E.g: 1.0.1"},"grading":{"name":"grading","type":"option","char":"g","description":"[isolated, incremental]","options":["isolated","incremental"]},"debug":{"name":"debug","type":"boolean","char":"d","description":"debugger mode for more verbage","allowNo":false}},"args":[]},"test":{"id":"test","description":"Test exercises","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false}},"args":[{"name":"exerciseSlug","description":"The name of the exercise to test","required":false,"hidden":false}]},"translate":{"id":"translate","description":"List all the lessons, the user is able of select many of them to translate to the given languages","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false}},"args":[]}}}
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.144",
4
+ "version": "5.0.148",
5
5
  "author": "Alejandro Sanchez @alesanchezr",
6
6
  "contributors": [
7
7
  {
@@ -37,6 +37,7 @@ import { RIGOBOT_HOST } from "../utils/api"
37
37
  import { minutesToISO8601Duration } from "../utils/misc"
38
38
  import { buildConfig, ConfigResponse } from "../utils/configBuilder"
39
39
  import { checkReadability, slugify } from "../utils/creatorUtilities"
40
+ import { checkAndFixSidebarPure } from "../utils/sidebarGenerator"
40
41
  const frontMatter = require("front-matter")
41
42
 
42
43
  dotenv.config()
@@ -125,6 +126,17 @@ export const processImage = async (
125
126
  }
126
127
  }
127
128
 
129
+ const createInitialSidebar = async (slugs: string[]) => {
130
+ const sidebar: Record<string, Record<string, string>> = {}
131
+ for (const slug of slugs) {
132
+ sidebar[slug] = {
133
+ us: slug,
134
+ }
135
+ }
136
+
137
+ return sidebar
138
+ }
139
+
128
140
  export async function processExercise(
129
141
  bucket: Bucket,
130
142
  rigoToken: string,
@@ -654,6 +666,89 @@ export default class ServeCommand extends SessionCommand {
654
666
  res.send({ message: "Files deleted" })
655
667
  })
656
668
 
669
+ app.get("/translations/sidebar", async (req, res) => {
670
+ const { slug } = req.query
671
+
672
+ const rigoToken = req.header("x-rigo-token")
673
+
674
+ console.log("GETTING SIDEBAR FOR", slug)
675
+
676
+ if (!slug) {
677
+ return res.status(400).json({ error: "Slug is required" })
678
+ }
679
+
680
+ const courseSlug = slug as string
681
+
682
+ try {
683
+ const file = bucket.file(`courses/${courseSlug}/.learn/sidebar.json`)
684
+
685
+ const [content] = await file.download()
686
+
687
+ const sidebar = JSON.parse(content.toString())
688
+
689
+ if (rigoToken) {
690
+ const { exercises } = await buildConfig(bucket, courseSlug)
691
+ const { fixedSidebar, valid } = await checkAndFixSidebarPure(
692
+ sidebar,
693
+ exercises,
694
+ rigoToken
695
+ )
696
+ if (fixedSidebar) {
697
+ await uploadFileToBucket(
698
+ bucket,
699
+ JSON.stringify(fixedSidebar),
700
+ `courses/${courseSlug}/.learn/sidebar.json`
701
+ )
702
+ return res.json(fixedSidebar)
703
+ }
704
+ }
705
+
706
+ res.json(sidebar)
707
+ } catch (error: any) {
708
+ if (error.code === 404) {
709
+ console.log("SIDEBAR FILE NOT FOUND", courseSlug)
710
+
711
+ const { exercises } = await buildConfig(bucket, courseSlug)
712
+
713
+ const exerciseSlugsArray = exercises.map(exercise => exercise.slug)
714
+ const sidebar = await createInitialSidebar(exerciseSlugsArray)
715
+
716
+ if (rigoToken) {
717
+ const { fixedSidebar } = await checkAndFixSidebarPure(
718
+ sidebar,
719
+ exercises,
720
+ rigoToken
721
+ )
722
+ if (fixedSidebar) {
723
+ await uploadFileToBucket(
724
+ bucket,
725
+ JSON.stringify(fixedSidebar),
726
+ `courses/${courseSlug}/.learn/sidebar.json`
727
+ )
728
+ }
729
+
730
+ await uploadFileToBucket(
731
+ bucket,
732
+ JSON.stringify(sidebar),
733
+ `courses/${courseSlug}/.learn/sidebar.json`
734
+ )
735
+ return res.status(200).json(fixedSidebar)
736
+ }
737
+
738
+ await uploadFileToBucket(
739
+ bucket,
740
+ JSON.stringify(sidebar),
741
+ `courses/${courseSlug}/.learn/sidebar.json`
742
+ )
743
+
744
+ return res.status(200).json(sidebar)
745
+ }
746
+
747
+ console.error("Unexpected error:", error)
748
+ res.status(500).json({ error: error.message })
749
+ }
750
+ })
751
+
657
752
  app.get("/test/:slug", (req, res) => {
658
753
  emitToCourse(req.params.slug, "course-creation", {
659
754
  lesson: "000-welcome-to-bird-domestication",
@@ -759,6 +854,19 @@ export default class ServeCommand extends SessionCommand {
759
854
  )
760
855
  await Promise.all(imagePromises)
761
856
 
857
+ const sidebar = await createInitialSidebar(
858
+ syllabus.lessons.map(lesson =>
859
+ slugify(lesson.id + "-" + lesson.title)
860
+ )
861
+ )
862
+ await uploadFileToBucket(
863
+ bucket,
864
+ JSON.stringify(sidebar),
865
+ `${tutorialDir}/.learn/sidebar.json`
866
+ )
867
+
868
+ console.log("SIDEBAR UPLOADED", sidebar)
869
+
762
870
  return res.json({
763
871
  message: "Course created",
764
872
  slug: slugify(syllabus.courseInfo.title),
@@ -126,6 +126,7 @@ function App() {
126
126
  courseInfo: {
127
127
  ...formState,
128
128
  title: res.parsed.title,
129
+ description: res.parsed.description,
129
130
  },
130
131
  })
131
132
  navigate("/creator/syllabus")
@@ -212,7 +213,7 @@ function App() {
212
213
  <>
213
214
  <div className="flex flex-col md:flex-row gap-2 justify-center">
214
215
  <SelectableCard
215
- title="Yes"
216
+ title="📄 Yes"
216
217
  onClick={() => {
217
218
  setFormState({
218
219
  hasContentIndex: true,
@@ -224,7 +225,7 @@ function App() {
224
225
  selected={false}
225
226
  />
226
227
  <SelectableCard
227
- title="No, help me create one"
228
+ title="🛠️ No, help me create one"
228
229
  onClick={() => {
229
230
  if (!formState.contentIndex && !formState.description) {
230
231
  toast.error(
@@ -274,8 +275,6 @@ function App() {
274
275
  return (
275
276
  <>
276
277
  <ParamsChecker />
277
- {/* <div id="turnstile-container" style={{ display: "none" }} /> */}
278
-
279
278
  {formState.isCompleted ? (
280
279
  <Loader
281
280
  text="Learnpack is setting up your tutorial. It may take a moment..."
@@ -16,6 +16,7 @@ export const ConsumablesManager = () => {
16
16
  const fetchConsumables = async () => {
17
17
  try {
18
18
  const consumables = await getConsumables(auth.bcToken)
19
+ console.log("CONSUMABLES", parseConsumables(consumables.voids))
19
20
  setConsumables(parseConsumables(consumables.voids))
20
21
  } catch (error) {
21
22
  setAuth({
@@ -1,21 +1,42 @@
1
- export const ContentCard = ({
1
+ import React from "react"
2
+
3
+ interface ContentCardProps {
4
+ description: string
5
+ icon: React.ReactNode
6
+ className?: string
7
+ children?: React.ReactNode
8
+ onClick?: () => void
9
+ onDragOver?: (e: React.DragEvent<HTMLDivElement>) => void
10
+ onDrop?: (e: React.DragEvent<HTMLDivElement>) => void
11
+ onDragEnter?: (e: React.DragEvent<HTMLDivElement>) => void
12
+ onDragLeave?: (e: React.DragEvent<HTMLDivElement>) => void
13
+ }
14
+
15
+ export const ContentCard: React.FC<ContentCardProps> = ({
2
16
  description,
3
17
  icon,
18
+ className = "",
19
+ children,
4
20
  onClick,
5
- }: {
6
- description: string
7
- icon: React.ReactNode
8
- onClick: () => void
21
+ onDragOver,
22
+ onDrop,
23
+ onDragEnter,
24
+ onDragLeave,
9
25
  }) => {
10
26
  return (
11
27
  <div
12
- className="flex flex-col items-center justify-center gap-2 bg-white card p-2 rounded-lg shadow-md min-w-[250px] w-full h-[150px] cursor-pointer"
28
+ className={`flex flex-col items-center justify-center gap-2 bg-white card p-4 rounded-lg shadow-md min-w-[250px] w-full h-[150px] cursor-pointer transition border border-gray-200 hover:shadow-lg ${className}`}
13
29
  onClick={onClick}
30
+ onDragOver={onDragOver}
31
+ onDrop={onDrop}
32
+ onDragEnter={onDragEnter}
33
+ onDragLeave={onDragLeave}
14
34
  >
15
35
  <div className="text-blue-500 flex justify-center items-center">
16
36
  {icon}
17
37
  </div>
18
- <span className="text-sm text-center">{description}</span>
38
+ <span className="text-sm text-center text-gray-700">{description}</span>
39
+ {children}
19
40
  </div>
20
41
  )
21
42
  }
@@ -1,4 +1,4 @@
1
- import React, { useRef } from "react"
1
+ import React, { useRef, useState } from "react"
2
2
  import * as pdfjsLib from "pdfjs-dist"
3
3
  import mammoth from "mammoth"
4
4
  import { SVGS } from "../assets/svgs"
@@ -29,6 +29,7 @@ const FileUploader: React.FC<FileUploaderProps> = ({
29
29
  styledAs = "button",
30
30
  }) => {
31
31
  const inputRef = useRef<HTMLInputElement>(null)
32
+ const [isDragging, setIsDragging] = useState(false)
32
33
 
33
34
  const extractText = async (file: File): Promise<ParsedFile> => {
34
35
  const { type, name } = file
@@ -60,42 +61,68 @@ const FileUploader: React.FC<FileUploaderProps> = ({
60
61
  return { name, text }
61
62
  }
62
63
 
63
- const handleFiles = async (e: React.ChangeEvent<HTMLInputElement>) => {
64
- const inputFiles = e.target.files
65
- if (!inputFiles) return
66
-
67
- const files = Array.from(inputFiles).filter((file) =>
64
+ const parseFiles = async (files: FileList | File[]) => {
65
+ const validFiles = Array.from(files).filter((file) =>
68
66
  allowedTypes.includes(file.type)
69
67
  )
70
- const parsed = await Promise.all(files.map(extractText))
68
+ const parsed = await Promise.all(validFiles.map(extractText))
69
+ console.log(parsed, "parsed files")
71
70
  onResult(parsed)
72
71
  }
73
72
 
73
+ const handleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
74
+ if (!e.target.files) return
75
+ parseFiles(e.target.files)
76
+ }
77
+
78
+ const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
79
+ e.preventDefault()
80
+ setIsDragging(false)
81
+ if (e.dataTransfer.files.length > 0) {
82
+ parseFiles(e.dataTransfer.files)
83
+ }
84
+ }
85
+
74
86
  return (
75
87
  <>
76
88
  {styledAs === "button" && (
77
- <button
78
- type="button"
79
- className="cursor-pointer blue-on-hover flex items-center justify-center w-6 h-6"
80
- onClick={() => inputRef.current?.click()}
89
+ <div
90
+ className="flex items-center justify-center"
91
+ onDragOver={() => setIsDragging(true)}
92
+ onDragLeave={() => setIsDragging(false)}
93
+ onDrop={handleDrop}
81
94
  >
82
- {SVGS.clip}
83
- </button>
95
+ <button
96
+ type="button"
97
+ className="cursor-pointer blue-on-hover flex items-center justify-center w-6 h-6"
98
+ onClick={() => inputRef.current?.click()}
99
+ >
100
+ {SVGS.clip}
101
+ </button>
102
+ </div>
84
103
  )}
85
104
  {styledAs === "card" && (
86
105
  <ContentCard
87
- description="Upload a PDF"
88
- icon={SVGS.pdf}
106
+ description={
107
+ isDragging ? "Drop it here" : "Upload a PDF or drag it here"
108
+ }
109
+ icon={isDragging ? SVGS.clip : SVGS.pdf}
89
110
  onClick={() => inputRef.current?.click()}
111
+ onDragOver={(e) => {
112
+ e.preventDefault()
113
+ setIsDragging(true)
114
+ }}
115
+ onDragLeave={() => setIsDragging(false)}
116
+ onDrop={handleDrop}
117
+ className={isDragging ? "border-blue-600 bg-blue-50" : ""}
90
118
  />
91
119
  )}
92
-
93
120
  <input
94
121
  ref={inputRef}
95
122
  type="file"
96
123
  multiple
97
124
  accept=".pdf,.docx,.txt,.md"
98
- onChange={handleFiles}
125
+ onChange={handleInput}
99
126
  style={{ display: "none" }}
100
127
  />
101
128
  </>
@@ -46,7 +46,7 @@ const Loader: React.FC<LoaderProps> = ({
46
46
  className={`flex flex-col items-center justify-center bg-gray-50 text-center space-y-6 ${minheight}`}
47
47
  >
48
48
  <div className="text-blue-500 text-4xl animate-pulse">{icon}</div>
49
- <p className="text-gray-800 text-lg whitespace-pre-line">{text}</p>
49
+ <p className="text-gray-800 text-lg whitespace-pre-line px-4">{text}</p>
50
50
  {!buffer && (
51
51
  <div className="flex space-x-2 mt-2">
52
52
  {[0, 1, 2, 3].map((i) => (
@@ -34,10 +34,10 @@ const PreviewGenerator: React.FC = () => {
34
34
  html2canvas(previewElement, {
35
35
  useCORS: true,
36
36
  }).then(async (canvas) => {
37
- // const anchor = document.createElement("a")
38
- // anchor.href = canvas.toDataURL("image/png")
39
- // anchor.download = "preview.png"
40
- // anchor.click()
37
+ const anchor = document.createElement("a")
38
+ anchor.href = canvas.toDataURL("image/png")
39
+ anchor.download = "preview.png"
40
+ anchor.click()
41
41
 
42
42
  const imageUrl = canvas.toDataURL("image/png")
43
43
 
@@ -55,7 +55,7 @@ const PreviewGenerator: React.FC = () => {
55
55
  }, [])
56
56
 
57
57
  return (
58
- <div className="fixed">
58
+ <div className="fixed bg-white z-50 border-2 border-gray-200 ">
59
59
  <div
60
60
  id="preview"
61
61
  style={{
@@ -64,18 +64,20 @@ const PreviewGenerator: React.FC = () => {
64
64
  background: "white",
65
65
  }}
66
66
  >
67
- <div className="bg-learnpack-blue p-4 rounded-md" />
68
- <div className="px-4 -mt-5">
69
- <h1 className="text-2xl font-bold">{syllabus.courseInfo?.title}</h1>
70
- <p className="mt-5 text-sm">{syllabus.courseInfo?.description}</p>
71
- <div className="flex items-center gap-2 mt-5">
67
+ <div className="bg-learnpack-blue p-10 rounded-md" />
68
+ <div className="px-20 -mt-5">
69
+ <h1 className="text-7xl font-bold mt-14">
70
+ {syllabus.courseInfo?.title}
71
+ </h1>
72
+ {/* <p className="mt-5 text-sm">{syllabus.courseInfo?.description}</p> */}
73
+ <div className="flex items-center gap-2 mt-10">
72
74
  <img
73
75
  src={proxify(auth.user?.profile?.avatar_url || "")}
74
76
  alt="Profile"
75
77
  className="w-10 h-10 rounded-full mt-3"
76
78
  />
77
79
  <div>
78
- <p className=" text-sm font-bold">
80
+ <p className=" text-sm font-bold ">
79
81
  Author: {auth.user?.first_name} {auth.user?.last_name}
80
82
  </p>
81
83
  <small className=" text-sm">
@@ -61,11 +61,11 @@ const TurnstileChallenge = ({
61
61
  return () => clearInterval(interval)
62
62
  }, [siteKey, autoStart, onSuccess, onError])
63
63
 
64
- // const reset = () => {
65
- // if (widgetId.current && window.turnstile) {
66
- // window.turnstile.reset(widgetId.current)
67
- // }
68
- // }
64
+ // const reset = () => {
65
+ // if (widgetId.current && window.turnstile) {
66
+ // window.turnstile.reset(widgetId.current)
67
+ // }
68
+ // }
69
69
 
70
70
  return (
71
71
  // <div className="flex flex-col gap-4 bg-white w-full fixed bottom-0 left-0 right-0 p-4 h-full items-center justify-center">
@@ -244,11 +244,11 @@ export const ContentIndex = ({
244
244
  {showScrollHint && !isThinking && (
245
245
  <div className="pointer-events-none relative">
246
246
  <div className="absolute bottom-0 left-0 w-full h-60 bg-gradient-to-t from-white to-transparent z-10" />
247
- <div className="absolute bottom-10 left-0 w-full flex justify-center z-20">
247
+ <div className="absolute bottom-10 left-0 w-full flex justify-center z-31">
248
248
  <button
249
249
  style={{ color: "#0084FF" }}
250
250
  onClick={() => scrollToBottom("bottom")}
251
- className="px-4 py-1 bg-white text-sm rounded hover:bg-blue-50 cursor-pointer pointer-events-auto font-bold flex items-center gap-2"
251
+ className="px-4 py-1 bg-white text-sm rounded hover:bg-blue-50 cursor-pointer pointer-events-auto font-bold flex items-center gap-2 "
252
252
  >
253
253
  Continue scrolling
254
254
  {SVGS.downArrow}
@@ -39,34 +39,34 @@ export const Sidebar = ({
39
39
  return (
40
40
  <>
41
41
  {!isOpen && (
42
- <>
43
- <div className="fixed bottom-5 left-2 z-50 lg:hidden">
44
- {showBubble && (
45
- <div
46
- className={`flex flex-row gap-3 cloudy bg-white rounded-md p-2 shadow-md bg-learnpack-blue duration-500 border-2 border-blue-600`}
47
- >
48
- <span>Chat with me to update the course content</span>
49
- <button
50
- className=" text-red-500 cursor-pointer bg-learnpack-blue p-2 rounded-md"
51
- onClick={() => setShowBubble(false)}
52
- >
53
- {SVGS.redClose}
54
- </button>
55
- </div>
56
- )}
57
- <button
58
- className="p-1 shadow-md cursor-pointer p-2 w-15 h-15 flex items-center justify-center bg-blue-600 rounded-[50%] fluid-svg"
59
- onClick={() => setIsOpen(true)}
42
+ <div className="fixed bottom-5 left-2 z-30 lg:hidden">
43
+ {showBubble && (
44
+ <div
45
+ className={`flex flex-row gap-3 cloudy bg-white rounded-md p-2 shadow-md bg-learnpack-blue duration-500 border-2 border-blue-600 mb-4`}
60
46
  >
61
- {SVGS.rigoSoftBlue}
62
- </button>
63
- </div>
64
- </>
47
+ <span className="w-[280px]">
48
+ Chat with me to update the course content
49
+ </span>
50
+ <button
51
+ className=" text-red-500 cursor-pointer bg-learnpack-blue p-2 rounded-md"
52
+ onClick={() => setShowBubble(false)}
53
+ >
54
+ {SVGS.redClose}
55
+ </button>
56
+ </div>
57
+ )}
58
+ <button
59
+ className="p-1 shadow-md cursor-pointer p-2 w-15 h-15 flex items-center justify-center bg-blue-600 rounded-[50%] fluid-svg"
60
+ onClick={() => setIsOpen(true)}
61
+ >
62
+ {SVGS.rigoSoftBlue}
63
+ </button>
64
+ </div>
65
65
  )}
66
66
 
67
67
  <div
68
68
  ref={sidebarRef}
69
- className={`fixed z-40 top-0 left-0 h-full w-4/5 max-w-sm bg-learnpack-blue text-sm text-gray-700 border-r border-C8DBFC overflow-y-auto scrollbar-hide p-6 transition-transform duration-300 ease-in-out lg:relative lg:transform-none lg:w-1/3 ${
69
+ className={`fixed z-30 top-0 left-0 h-full w-4/5 max-w-sm bg-learnpack-blue text-sm text-gray-700 border-r border-C8DBFC overflow-y-auto scrollbar-hide p-6 transition-transform duration-300 ease-in-out lg:relative lg:transform-none lg:w-1/3 ${
70
70
  isOpen ? "translate-x-0" : "-translate-x-full lg:translate-x-0"
71
71
  }`}
72
72
  >
@@ -146,7 +146,10 @@ const SyllabusEditor: React.FC = () => {
146
146
  setShowLoginModal(true)
147
147
  return
148
148
  }
149
- const success = await useConsumableCall(auth.bcToken, "ai-generation")
149
+ const success = await useConsumableCall(
150
+ auth.bcToken,
151
+ "ai-course-generation"
152
+ )
150
153
  if (!success) {
151
154
  toast.error("You don't have enough credits to generate a course!")
152
155
  return
@@ -35,11 +35,6 @@ body {
35
35
  background-color: #f4f9fe;
36
36
  }
37
37
 
38
- h1 {
39
- font-size: 3.2em;
40
- line-height: 1.1;
41
- }
42
-
43
38
  .bg-learnpack-blue {
44
39
  background-color: var(--soft-blue);
45
40
  }