@learnpack/learnpack 5.0.240 → 5.0.246

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.
@@ -1,6 +1,5 @@
1
1
  import { flags } from "@oclif/command"
2
2
 
3
- // import { readDocument } from "../utils/readDocuments"
4
3
  import { YoutubeTranscript } from "youtube-transcript"
5
4
  import * as express from "express"
6
5
  import * as cors from "cors"
@@ -31,6 +30,7 @@ import {
31
30
  generateImage,
32
31
  isPackageAuthor,
33
32
  createStructuredPreviewReadme,
33
+ translateCourseMetadata,
34
34
  } from "../utils/rigoActions"
35
35
  import * as dotenv from "dotenv"
36
36
  import {
@@ -65,15 +65,13 @@ function findLast<T>(
65
65
  export const createLearnJson = (courseInfo: FormState) => {
66
66
  // console.log("courseInfo to create learn json", courseInfo)
67
67
 
68
- const expectedPreviewUrl = `https://${slugify(
69
- courseInfo.title as string
70
- )}.learn-pack.com/preview.png`
68
+ const expectedPreviewUrl = `https://${courseInfo.slug}.learn-pack.com/preview.png`
71
69
  console.log("Preview url in generated learn.json", expectedPreviewUrl)
72
70
 
73
71
  const language = courseInfo.language || "en"
74
72
 
75
73
  const learnJson = {
76
- slug: slugify(courseInfo.title as string),
74
+ slug: courseInfo.slug,
77
75
  title:
78
76
  language === "en" || language === "us" ?
79
77
  {
@@ -106,27 +104,6 @@ const uploadFileToBucket = async (
106
104
  await fileRef.save(Buffer.from(file, "utf8"))
107
105
  }
108
106
 
109
- const uploadImageToBucket = async (
110
- bucket: Bucket,
111
- url: string,
112
- path: string
113
- ) => {
114
- const response = await fetch(url)
115
- if (!response.ok) {
116
- throw new Error(`Failed to download image: ${response.statusText}`)
117
- }
118
-
119
- const contentType =
120
- response.headers.get("content-type") || "application/octet-stream"
121
- const buffer = await response.arrayBuffer()
122
-
123
- const fileRef = bucket.file(path)
124
- await fileRef.save(Buffer.from(buffer), {
125
- resumable: false,
126
- contentType,
127
- })
128
- }
129
-
130
107
  const PARAMS = {
131
108
  expected_grade_level: "8",
132
109
  max_fkgl: 10,
@@ -135,41 +112,6 @@ const PARAMS = {
135
112
  max_title_length: 50,
136
113
  }
137
114
 
138
- // app.post("/webhooks/:courseSlug/images/:imageId", async (req, res) => {
139
- // const { courseSlug, imageId } = req.params
140
- // const body = req.body
141
- // console.log("RECEIVING IMAGE WEBHOOK", body)
142
-
143
- // const imageUrl = body.image_url
144
- // const imagePath = `courses/${courseSlug}/.learn/assets/${imageId}`
145
-
146
- // const imageFile = bucket.file(imagePath)
147
- // const [exists] = await imageFile.exists()
148
- // if (!exists) {
149
- // // Descargar la imagen
150
- // const response = await fetch(imageUrl)
151
- // if (!response.ok) {
152
- // return res.status(400).json({
153
- // status: "ERROR",
154
- // message: "No se pudo descargar la imagen",
155
- // })
156
- // }
157
-
158
- // const buffer = await response.arrayBuffer()
159
-
160
- // // Guardar la imagen en el bucket
161
- // await imageFile.save(new Uint8Array(buffer), {
162
- // contentType: "image/png",
163
- // })
164
- // }
165
-
166
- // emitToNotification(imageId, {
167
- // status: "SUCCESS",
168
- // message: "Image generated successfully",
169
- // })
170
- // res.json({ status: "SUCCESS" })
171
- // })
172
-
173
115
  export const processImage = async (
174
116
  url: string,
175
117
  description: string,
@@ -186,7 +128,6 @@ export const processImage = async (
186
128
  prompt: description,
187
129
  callbackUrl: webhookUrl,
188
130
  })
189
- // await uploadImageToBucket(bucket, res.image_url, imagePath)
190
131
 
191
132
  // console.log("✅ Image", imagePath, "generated successfully!")
192
133
  return true
@@ -236,7 +177,6 @@ const uploadInitialReadme = async (
236
177
  }
237
178
 
238
179
  const cleanFormState = (formState: FormState) => {
239
- // keysToDelete: description, technologies, purpose
240
180
  const {
241
181
  description,
242
182
  technologies,
@@ -252,6 +192,45 @@ const cleanFormState = (formState: FormState) => {
252
192
  return rest
253
193
  }
254
194
 
195
+ const createMultiLangAsset = async (
196
+ bucket: Bucket,
197
+ rigoToken: string,
198
+ bcToken: string,
199
+ courseSlug: string,
200
+ courseJson: any,
201
+ deployUrl: string
202
+ ) => {
203
+ const availableLangs = Object.keys(courseJson.title)
204
+ console.log("AVAILABLE LANGUAGES to upload asset", availableLangs)
205
+
206
+ const all_translations: string[] = []
207
+
208
+ for (const lang of availableLangs) {
209
+ // eslint-disable-next-line no-await-in-loop
210
+ const indexReadme = await bucket.file(
211
+ `courses/${courseSlug}/README.${
212
+ lang === "us" || lang === "en" ? "md" : `${lang}.md`
213
+ }`
214
+ )
215
+ // eslint-disable-next-line no-await-in-loop
216
+ const [indexReadmeContent] = await indexReadme.download()
217
+ const indexReadmeString = indexReadmeContent.toString()
218
+ const b64IndexReadme = Buffer.from(indexReadmeString).toString("base64")
219
+
220
+ // eslint-disable-next-line no-await-in-loop
221
+ const asset = await handleAssetCreation(
222
+ { token: bcToken, rigobotToken: rigoToken.trim() },
223
+ courseJson,
224
+ lang,
225
+ deployUrl,
226
+ b64IndexReadme,
227
+ all_translations
228
+ )
229
+
230
+ all_translations.push(asset.slug)
231
+ }
232
+ }
233
+
255
234
  async function startExerciseGeneration(
256
235
  bucket: Bucket,
257
236
  rigoToken: string,
@@ -544,7 +523,6 @@ export default class ServeCommand extends SessionCommand {
544
523
  const { courseSlug } = req.params
545
524
  const body = req.body
546
525
 
547
- console.log("RECEIVING INITIAL README WEBHOOK", body)
548
526
  // Save the file as courses/courseSlug/README.md
549
527
  const filePath = `courses/${courseSlug}/README.${
550
528
  body.parsed.language_code === "us" ||
@@ -935,25 +913,34 @@ export default class ServeCommand extends SessionCommand {
935
913
 
936
914
  app.post("/actions/translate", express.json(), async (req, res) => {
937
915
  console.log("POST /actions/translate")
938
- const { exerciseSlugs, languages, rigoToken } = req.body
916
+ const { exerciseSlugs, languages, rigoToken, currentLanguage } = req.body
939
917
  const query = req.query
940
918
  const courseSlug = query.slug
941
919
 
942
- console.log("EXERCISE SLUGS", exerciseSlugs)
943
- console.log("LANGUAGES", languages)
944
- console.log("RIGO TOKEN", rigoToken)
945
-
946
920
  if (!rigoToken) {
947
921
  return res.status(400).json({ error: "RigoToken not found" })
948
922
  }
949
923
 
950
924
  const languagesToTranslate: string[] = languages.split(",")
951
925
 
926
+ const course = await bucket
927
+ .file(`courses/${courseSlug}/learn.json`)
928
+ .download()
929
+
930
+ const courseJson = JSON.parse(course.toString())
931
+ const languageCodes = new Set()
932
+
952
933
  try {
953
934
  await Promise.all(
954
935
  exerciseSlugs.map(async (slug: string) => {
955
- const readmePath = `courses/${courseSlug}/exercises/${slug}/README.md`
936
+ const readmePath = `courses/${courseSlug}/exercises/${slug}/README${
937
+ currentLanguage === "us" || currentLanguage === "en" ?
938
+ "" :
939
+ `.${currentLanguage}`
940
+ }.md`
941
+
956
942
  const readme = await bucket.file(readmePath).download()
943
+
957
944
  await Promise.all(
958
945
  languagesToTranslate.map(async (language: string) => {
959
946
  const response = await translateExercise(rigoToken, {
@@ -963,14 +950,98 @@ export default class ServeCommand extends SessionCommand {
963
950
  })
964
951
 
965
952
  const translatedReadme = await bucket.file(
966
- `courses/${courseSlug}/exercises/${slug}/README.${response.parsed.output_language_code}.md`
953
+ `courses/${courseSlug}/exercises/${slug}/README${
954
+ response.parsed.output_language_code === "us" ||
955
+ response.parsed.output_language_code === "en" ?
956
+ "" :
957
+ `.${response.parsed.output_language_code}`
958
+ }.md`
967
959
  )
968
960
  await translatedReadme.save(response.parsed.translation)
961
+
962
+ languageCodes.add(response.parsed.output_language_code)
969
963
  })
970
964
  )
971
965
  })
972
966
  )
973
967
 
968
+ if (languageCodes.has("en")) {
969
+ languageCodes.delete("en")
970
+ languageCodes.add("us")
971
+ }
972
+
973
+ const currentLanguages = Object.keys(courseJson.title)
974
+ for (const languageCode of currentLanguages) {
975
+ if (languageCodes.has(languageCode)) {
976
+ languageCodes.delete(languageCode)
977
+ }
978
+ }
979
+
980
+ if ([...languageCodes].length > 0) {
981
+ console.log("TRANSLATING COURSE METADATA", languageCodes)
982
+
983
+ const translatedCourseMetadata = await Promise.all(
984
+ [...languageCodes].map(async languageCode => {
985
+ const result = await translateCourseMetadata(rigoToken, {
986
+ title: courseJson.title[currentLanguage],
987
+ description: courseJson.description[currentLanguage],
988
+ destination_lang_code: languageCode as string,
989
+ })
990
+
991
+ return {
992
+ languageCode,
993
+ title: result.parsed.title,
994
+ description: result.parsed.description,
995
+ }
996
+ })
997
+ )
998
+
999
+ for (const metadata of translatedCourseMetadata) {
1000
+ courseJson.title[metadata.languageCode as string] = metadata.title
1001
+ courseJson.description[metadata.languageCode as string] =
1002
+ metadata.description
1003
+ }
1004
+
1005
+ await uploadFileToBucket(
1006
+ bucket,
1007
+ JSON.stringify(courseJson),
1008
+ `courses/${courseSlug}/learn.json`
1009
+ )
1010
+
1011
+ const previewReadme = await bucket.file(
1012
+ `courses/${courseSlug}/README${
1013
+ currentLanguage === "us" || currentLanguage === "en" ?
1014
+ "" :
1015
+ `.${currentLanguage}`
1016
+ }.md`
1017
+ )
1018
+ const [previewReadmeContent] = await previewReadme.download()
1019
+ const previewReadmeContentString = previewReadmeContent.toString()
1020
+
1021
+ await Promise.all(
1022
+ [...languageCodes].map(async languageCode => {
1023
+ const translatedPreviewReadme = await translateExercise(
1024
+ rigoToken,
1025
+ {
1026
+ text_to_translate: previewReadmeContentString,
1027
+ output_language: languageCode as string,
1028
+ exercise_slug: "preview-readme",
1029
+ }
1030
+ )
1031
+
1032
+ await bucket
1033
+ .file(
1034
+ `courses/${courseSlug}/README${
1035
+ languageCode === "us" || languageCode === "en" ?
1036
+ "" :
1037
+ `.${languageCode}`
1038
+ }.md`
1039
+ )
1040
+ .save(translatedPreviewReadme.parsed.translation)
1041
+ })
1042
+ )
1043
+ }
1044
+
974
1045
  return res.status(200).json({ message: "Translated exercises" })
975
1046
  } catch (error) {
976
1047
  console.log(error, "ERROR")
@@ -979,6 +1050,8 @@ export default class ServeCommand extends SessionCommand {
979
1050
  })
980
1051
 
981
1052
  app.delete("/exercise/:slug/delete", async (req, res) => {
1053
+ console.log("DELETE /exercise/:slug/delete")
1054
+
982
1055
  const { slug } = req.params
983
1056
  const query = req.query
984
1057
  const courseSlug = query.slug
@@ -1032,13 +1105,17 @@ export default class ServeCommand extends SessionCommand {
1032
1105
  res.json(sidebar)
1033
1106
  } catch (error: any) {
1034
1107
  if (error.code === 404) {
1035
- console.log("SIDEBAR FILE NOT FOUND", courseSlug)
1036
-
1037
1108
  const { exercises } = await buildConfig(bucket, courseSlug)
1038
1109
 
1039
1110
  const exerciseSlugsArray = exercises.map(exercise => exercise.slug)
1040
1111
  const sidebar = await createInitialSidebar(exerciseSlugsArray)
1041
1112
 
1113
+ await uploadFileToBucket(
1114
+ bucket,
1115
+ JSON.stringify(sidebar),
1116
+ `courses/${courseSlug}/.learn/sidebar.json`
1117
+ )
1118
+
1042
1119
  if (rigoToken) {
1043
1120
  const { fixedSidebar } = await checkAndFixSidebarPure(
1044
1121
  sidebar,
@@ -1053,20 +1130,9 @@ export default class ServeCommand extends SessionCommand {
1053
1130
  )
1054
1131
  }
1055
1132
 
1056
- await uploadFileToBucket(
1057
- bucket,
1058
- JSON.stringify(sidebar),
1059
- `courses/${courseSlug}/.learn/sidebar.json`
1060
- )
1061
1133
  return res.status(200).json(fixedSidebar)
1062
1134
  }
1063
1135
 
1064
- await uploadFileToBucket(
1065
- bucket,
1066
- JSON.stringify(sidebar),
1067
- `courses/${courseSlug}/.learn/sidebar.json`
1068
- )
1069
-
1070
1136
  return res.status(200).json(sidebar)
1071
1137
  }
1072
1138
 
@@ -1075,35 +1141,6 @@ export default class ServeCommand extends SessionCommand {
1075
1141
  }
1076
1142
  })
1077
1143
 
1078
- // app.get("/test/:slug", (req, res) => {
1079
- // emitToCourse(req.params.slug, "course-creation", {
1080
- // lesson: "000-welcome-to-bird-domestication",
1081
- // status: "generating",
1082
- // log: "Hello",
1083
- // })
1084
- // emitToCourse(req.params.slug, "course-creation", {
1085
- // lesson: "000-welcome-to-bird-domestication",
1086
- // status: "generating",
1087
- // log: "Hello",
1088
- // })
1089
- // emitToCourse(req.params.slug, "course-creation", {
1090
- // lesson: "000-welcome-to-bird-domestication",
1091
- // status: "generating",
1092
- // log: "Hello broder",
1093
- // })
1094
- // emitToCourse(req.params.slug, "course-creation", {
1095
- // lesson: "000-welcome-to-bird-domestication",
1096
- // status: "done",
1097
- // log: "Hello broder",
1098
- // })
1099
- // emitToCourse(req.params.slug, "course-creation", {
1100
- // lesson: "other",
1101
- // status: "generating",
1102
- // log: "Hello",
1103
- // })
1104
- // res.send({ message: "Course creation started" })
1105
- // })
1106
-
1107
1144
  app.get("/technologies", async (req, res) => {
1108
1145
  console.log("GET /technologies")
1109
1146
 
@@ -1143,7 +1180,7 @@ export default class ServeCommand extends SessionCommand {
1143
1180
  return res.status(400).json({ error: "Missing tokens" })
1144
1181
  }
1145
1182
 
1146
- const courseSlug = slugify(syllabus.courseInfo.title)
1183
+ const courseSlug = syllabus.courseInfo.slug
1147
1184
 
1148
1185
  const tutorialDir = `courses/${courseSlug}`
1149
1186
  const learnJson = createLearnJson(syllabus.courseInfo)
@@ -1241,7 +1278,7 @@ export default class ServeCommand extends SessionCommand {
1241
1278
 
1242
1279
  return res.json({
1243
1280
  message: "Course created",
1244
- slug: slugify(syllabus.courseInfo.title),
1281
+ slug: syllabus.courseInfo.slug,
1245
1282
  })
1246
1283
  })
1247
1284
 
@@ -1356,6 +1393,7 @@ export default class ServeCommand extends SessionCommand {
1356
1393
  app.post("/actions/publish/:slug", async (req, res) => {
1357
1394
  try {
1358
1395
  const { slug } = req.params
1396
+ const { currentLanguage } = req.body
1359
1397
  const rigoToken = req.header("x-rigo-token")
1360
1398
  const bcToken = req.header("x-breathecode-token")
1361
1399
  // const { academyId, categoryId } = req.body
@@ -1371,8 +1409,6 @@ export default class ServeCommand extends SessionCommand {
1371
1409
  slug
1372
1410
  )
1373
1411
 
1374
- // const fixedSlug = fixSlugLength(slug)
1375
-
1376
1412
  const prefix = `courses/${slug}/`
1377
1413
  const tmpRoot = path.join(os.tmpdir(), `learnpack-${slug}`)
1378
1414
  const buildRoot = path.join(tmpRoot, "build")
@@ -1409,11 +1445,6 @@ export default class ServeCommand extends SessionCommand {
1409
1445
  selectedLang = availableLangs[0]
1410
1446
  }
1411
1447
 
1412
- // console.log(availableLangs, "AVAILABLE LANGs")
1413
- // console.log(selectedLang, "SELECTED LANG")
1414
- // console.log(title, "TITLE")
1415
-
1416
- // 5) Inyectar placeholders en index.html
1417
1448
  const idxTpl = fs.readFileSync(path.join(uiSrc, "index.html"), "utf-8")
1418
1449
  const idxHtml = idxTpl
1419
1450
  .replace(/{{title}}/g, title)
@@ -1480,29 +1511,18 @@ export default class ServeCommand extends SessionCommand {
1480
1511
  {
1481
1512
  headers: {
1482
1513
  ...form.getHeaders(),
1483
- Authorization: `Token ${rigoToken.trim()}`,
1514
+ Authorization: "Token " + rigoToken.trim(),
1484
1515
  },
1485
1516
  }
1486
1517
  )
1487
1518
 
1488
- const indexReadme = await bucket.file(
1489
- `courses/${slug}/README.${
1490
- selectedLang === "us" || selectedLang === "en" ?
1491
- "md" :
1492
- `${selectedLang}.md`
1493
- }`
1494
- )
1495
- const [indexReadmeContent] = await indexReadme.download()
1496
- const indexReadmeString = indexReadmeContent.toString()
1497
- const b64IndexReadme =
1498
- Buffer.from(indexReadmeString).toString("base64")
1499
-
1500
- await handleAssetCreation(
1501
- { token: bcToken, rigobotToken: rigoToken },
1519
+ await createMultiLangAsset(
1520
+ bucket,
1521
+ rigoToken,
1522
+ bcToken,
1523
+ slug,
1502
1524
  fullConfig.config,
1503
- selectedLang,
1504
- rigoRes.data.url,
1505
- b64IndexReadme
1525
+ rigoRes.data.url
1506
1526
  )
1507
1527
 
1508
1528
  rimraf.sync(tmpRoot)
@@ -26,6 +26,7 @@ import ResumeCourseModal from "./components/ResumeCourseModal"
26
26
  import { possiblePurposes, PurposeSelector } from "./components/PurposeSelector"
27
27
  import { useTranslation } from "react-i18next"
28
28
  import NotificationListener from "./components/NotificationListener"
29
+ import { slugify } from "./utils/creatorUtils"
29
30
 
30
31
  function App() {
31
32
  const navigate = useNavigate()
@@ -410,7 +411,8 @@ function App() {
410
411
  lessons,
411
412
  courseInfo: {
412
413
  ...formState,
413
- title: fixTitleLength(res.parsed.title),
414
+ title: res.parsed.title,
415
+ slug: slugify(fixTitleLength(res.parsed.title)),
414
416
  description: res.parsed.description,
415
417
  language:
416
418
  res.parsed.languageCode || formState.language || "en",
@@ -13,7 +13,7 @@ const NotificationListener: React.FC<NotificationListenerProps> = ({
13
13
  onNotification,
14
14
  }) => {
15
15
  useEffect(() => {
16
- console.log("NOTIFICATION ID listening", notificationId)
16
+
17
17
  if (!notificationId) return
18
18
 
19
19
  socketClient.connect()
@@ -13,7 +13,6 @@ import {
13
13
  // reWriteTitle,
14
14
  useConsumableCall,
15
15
  isValidRigoToken,
16
- fixTitleLength,
17
16
  } from "../../utils/lib"
18
17
 
19
18
  import Loader from "../Loader"
@@ -96,25 +95,23 @@ const SyllabusEditor: React.FC = () => {
96
95
  return
97
96
  }
98
97
 
99
- let slug = slugify(syllabus.courseInfo.title)
100
- let newTitle = syllabus.courseInfo.title
101
- let isAvailable = await isSlugAvailable(slug)
98
+ let newSlug = syllabus.courseInfo.slug
99
+ let isAvailable = await isSlugAvailable(syllabus.courseInfo.slug)
102
100
 
103
101
  while (!isAvailable) {
104
- if (newTitle.length > 50) {
105
- newTitle = newTitle.slice(0, 44)
102
+ if (newSlug.length > 50) {
103
+ newSlug = newSlug.slice(0, 44)
106
104
  }
107
- newTitle = newTitle + "-" + randomUUID()
108
- newTitle = newTitle.slice(0, 50)
109
- slug = slugify(newTitle)
110
- isAvailable = await isSlugAvailable(slug)
105
+ newSlug = newSlug + "-" + randomUUID()
106
+ newSlug = newSlug.slice(0, 50)
107
+ isAvailable = await isSlugAvailable(newSlug)
111
108
  }
112
109
 
113
110
  push({
114
111
  ...syllabus,
115
112
  courseInfo: {
116
113
  ...syllabus.courseInfo,
117
- title: newTitle,
114
+ slug: newSlug,
118
115
  },
119
116
  })
120
117
  }
@@ -215,9 +212,7 @@ const SyllabusEditor: React.FC = () => {
215
212
 
216
213
  const timeout = setTimeout(() => {
217
214
  cleanAll()
218
- window.location.href = `/preview/${slugify(
219
- syllabus.courseInfo.title || ""
220
- )}?token=${auth.bcToken}&language=${syllabus.courseInfo.language}`
215
+ window.location.href = `/preview/${syllabus.courseInfo.slug}?token=${auth.bcToken}&language=${syllabus.courseInfo.language}`
221
216
  }, 8000)
222
217
  try {
223
218
  await createCourse(syllabus, tokenToUse, auth.bcToken)
@@ -230,6 +225,8 @@ const SyllabusEditor: React.FC = () => {
230
225
 
231
226
  if (!syllabus) return null
232
227
 
228
+ console.log(syllabus.courseInfo)
229
+
233
230
  return isGenerating ? (
234
231
  <>
235
232
  <Loader
@@ -253,8 +250,7 @@ It may take a moment..."
253
250
  lessons: lessons,
254
251
  courseInfo: {
255
252
  ...syllabus.courseInfo,
256
- title:
257
- fixTitleLength(res.parsed.title) || syllabus.courseInfo.title,
253
+ title: res.parsed.title || syllabus.courseInfo.title,
258
254
  description:
259
255
  res.parsed.description || syllabus.courseInfo.description,
260
256
  language:
@@ -19,8 +19,9 @@ export const publicInteractiveCreation = async (
19
19
  const webhookUrl = `${
20
20
  DEV_MODE
21
21
  ? "https://9cw5zmww-3000.use2.devtunnels.ms"
22
- : window.location.origin
23
- // "https://9cw5zmww-3000.use2.devtunnels.ms"
22
+ : // TODO: remove this
23
+ // : window.location.origin
24
+ "https://9cw5zmww-3000.use2.devtunnels.ms"
24
25
  }/notifications/${randomUID}`
25
26
 
26
27
  const response = await axios.post(
@@ -11,6 +11,7 @@ export type FormState = {
11
11
  hasContentIndex: boolean
12
12
  contentIndex: string
13
13
  purpose: string
14
+ slug: string
14
15
  language?: string
15
16
  isCompleted: boolean
16
17
  variables: string[]
@@ -85,6 +86,7 @@ const useStore = create<Store>()(
85
86
  formState: {
86
87
  description: "",
87
88
  duration: 0,
89
+ slug: "",
88
90
  hasContentIndex: false,
89
91
  contentIndex: "",
90
92
  purpose: "",
@@ -119,6 +121,7 @@ const useStore = create<Store>()(
119
121
  resetFormState: () =>
120
122
  set({
121
123
  formState: {
124
+ slug: "",
122
125
  currentStep: "description",
123
126
  description: "",
124
127
  duration: 0,
@@ -169,7 +172,7 @@ const useStore = create<Store>()(
169
172
  formState: {
170
173
  description: "",
171
174
  duration: 0,
172
-
175
+ slug: "",
173
176
  language: "en",
174
177
  technologies: [],
175
178
  hasContentIndex: false,