@learnpack/learnpack 5.0.198 → 5.0.204

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.
Files changed (39) hide show
  1. package/README.md +13 -13
  2. package/lib/commands/serve.d.ts +5 -28
  3. package/lib/commands/serve.js +45 -14
  4. package/lib/creatorDist/assets/index-hTGEtFj4.js +38491 -0
  5. package/lib/creatorDist/index.html +1 -1
  6. package/lib/models/creator.d.ts +30 -0
  7. package/lib/models/creator.js +2 -0
  8. package/lib/utils/creatorUtilities.js +3 -2
  9. package/oclif.manifest.json +1 -1
  10. package/package.json +1 -1
  11. package/src/commands/serve.ts +58 -49
  12. package/src/creator/package-lock.json +97 -1
  13. package/src/creator/package.json +3 -0
  14. package/src/creator/src/App.tsx +48 -22
  15. package/src/creator/src/components/FileUploader.tsx +6 -5
  16. package/src/creator/src/components/LinkUploader.tsx +3 -1
  17. package/src/creator/src/components/Login.tsx +33 -27
  18. package/src/creator/src/components/PurposeSelector.tsx +32 -25
  19. package/src/creator/src/components/StepWizard.tsx +8 -4
  20. package/src/creator/src/components/Uploader.tsx +8 -5
  21. package/src/creator/src/components/syllabus/ContentIndex.tsx +17 -11
  22. package/src/creator/src/components/syllabus/Sidebar.tsx +4 -3
  23. package/src/creator/src/components/syllabus/SyllabusEditor.tsx +18 -2
  24. package/src/creator/src/i18n.ts +28 -0
  25. package/src/creator/src/locales/en.json +110 -0
  26. package/src/creator/src/locales/es.json +110 -0
  27. package/src/creator/src/main.tsx +1 -0
  28. package/src/creator/src/utils/creatorUtils.ts +2 -2
  29. package/src/creator/src/utils/lib.ts +7 -0
  30. package/src/creator/src/utils/store.ts +8 -5
  31. package/src/creatorDist/assets/index-hTGEtFj4.js +38491 -0
  32. package/src/creatorDist/index.html +1 -1
  33. package/src/models/creator.ts +32 -0
  34. package/src/ui/_app/app.css +1 -1
  35. package/src/ui/_app/app.js +55 -55
  36. package/src/ui/app.tar.gz +0 -0
  37. package/src/utils/creatorUtilities.ts +4 -4
  38. package/lib/creatorDist/assets/index-DszesaEz.js +0 -35438
  39. package/src/creatorDist/assets/index-DszesaEz.js +0 -35438
@@ -12,6 +12,7 @@ import {
12
12
  isValidRigoToken,
13
13
  loginWithToken,
14
14
  parseLesson,
15
+ fixTitleLength,
15
16
  } from "./utils/lib"
16
17
 
17
18
  import { Uploader } from "./components/Uploader"
@@ -22,9 +23,11 @@ import TurnstileChallenge from "./components/TurnstileChallenge"
22
23
  // import TurnstileChallenge from "./components/TurnstileChallenge"
23
24
  import ResumeCourseModal from "./components/ResumeCourseModal"
24
25
  import { possiblePurposes, PurposeSelector } from "./components/PurposeSelector"
26
+ import { useTranslation } from "react-i18next"
25
27
 
26
28
  function App() {
27
29
  const navigate = useNavigate()
30
+ const { t, i18n } = useTranslation()
28
31
 
29
32
  const {
30
33
  formState,
@@ -82,11 +85,12 @@ function App() {
82
85
  }
83
86
 
84
87
  const checkDescription = () => {
85
- const { description, duration, plan, purpose } = checkParams([
88
+ const { description, duration, plan, purpose, language } = checkParams([
86
89
  "description",
87
90
  "duration",
88
91
  "plan",
89
92
  "purpose",
93
+ "language",
90
94
  ])
91
95
  if (description) {
92
96
  console.log("description", description)
@@ -114,6 +118,12 @@ function App() {
114
118
  console.debug("No plan received in params")
115
119
  }
116
120
 
121
+ if (language && language.length === 2) {
122
+ setFormState({
123
+ language: language,
124
+ })
125
+ }
126
+
117
127
  if (purpose && purpose.length > 0 && possiblePurposes.includes(purpose)) {
118
128
  setFormState({
119
129
  purpose: purpose,
@@ -137,8 +147,8 @@ function App() {
137
147
 
138
148
  const res = await publicInteractiveCreation(
139
149
  {
140
- courseInfo: `${JSON.stringify(formState)} `,
141
- prevInteractions: "",
150
+ courseInfo: `${JSON.stringify(formState)}`,
151
+ prevInteractions: "USER: " + formState.description,
142
152
  },
143
153
  auth.rigoToken && isValid ? auth.rigoToken : auth.publicToken,
144
154
  formState.purpose || "learnpack-lesson-writer",
@@ -148,16 +158,21 @@ function App() {
148
158
  return parseLesson(lesson, [])
149
159
  })
150
160
 
151
- console.log("RES FROM RIGO", res)
152
161
  push({
153
162
  lessons,
154
163
  courseInfo: {
155
164
  ...formState,
156
- title: res.parsed.title,
165
+ title: fixTitleLength(res.parsed.title),
157
166
  description: res.parsed.description,
167
+ language: res.parsed.languageCode || formState.language || "en",
168
+ technologies: res.parsed.technologies,
158
169
  },
159
- // messages: [],
160
170
  })
171
+
172
+ if (res.parsed.languageCode) {
173
+ i18n.changeLanguage(res.parsed.languageCode)
174
+ }
175
+
161
176
  navigate("/creator/syllabus")
162
177
  setFormState({
163
178
  isCompleted: false,
@@ -195,14 +210,14 @@ function App() {
195
210
  const buildSteps = () => {
196
211
  const steps = [
197
212
  {
198
- title: "What do you want to learn?",
213
+ title: t("stepWizard.description"),
199
214
  slug: "description",
200
215
  isCompleted: formState.description.length > 0,
201
216
  required: true,
202
217
  content: (
203
218
  <textarea
204
219
  required
205
- placeholder="Describe your course"
220
+ placeholder={t("stepWizard.descriptionPlaceholder")}
206
221
  className="w-full h-24 border-2 border-gray-300 rounded-md p-2 bg-white"
207
222
  value={formState.description}
208
223
  onChange={(e) => {
@@ -215,14 +230,14 @@ function App() {
215
230
  },
216
231
 
217
232
  {
218
- title: "What is the estimated duration for this tutorial?",
233
+ title: t("stepWizard.duration"),
219
234
  slug: "duration",
220
235
  isCompleted: formState.duration > 0,
221
236
  required: true,
222
237
  content: (
223
238
  <div className="flex flex-col md:flex-row gap-2">
224
239
  <SelectableCard
225
- title="Around 30 minutes"
240
+ title={t("stepWizard.durationCard.30")}
226
241
  // subtitle="This is a tutorial that will take 30 minutes to complete"
227
242
  onClick={() => {
228
243
  setFormState({
@@ -233,7 +248,7 @@ function App() {
233
248
  selected={formState.duration === 30}
234
249
  />
235
250
  <SelectableCard
236
- title="Around 1 hour"
251
+ title={t("stepWizard.durationCard.60")}
237
252
  // subtitle="This is a tutorial that will take 1 hour to complete"
238
253
  onClick={() => {
239
254
  setFormState({
@@ -244,7 +259,7 @@ function App() {
244
259
  selected={formState.duration === 60}
245
260
  />
246
261
  <SelectableCard
247
- title="Around 2 hours"
262
+ title={t("stepWizard.durationCard.120")}
248
263
  // subtitle="This is a tutorial that will take 2 hours to complete"
249
264
  onClick={() => {
250
265
  setFormState({
@@ -258,7 +273,7 @@ function App() {
258
273
  ),
259
274
  },
260
275
  {
261
- title: "How would you like to use learnpack?",
276
+ title: t("stepWizard.purpose"),
262
277
  slug: "purpose",
263
278
  isCompleted: formState?.purpose?.length > 0,
264
279
  required: true,
@@ -274,7 +289,7 @@ function App() {
274
289
  ),
275
290
  },
276
291
  {
277
- title: "Please verify you are a human",
292
+ title: t("stepWizard.verifyHuman"),
278
293
  slug: "verifyHuman",
279
294
  isCompleted: false,
280
295
  required: true,
@@ -286,7 +301,7 @@ function App() {
286
301
  onSuccess={async (token) => {
287
302
  const { human, message, token: jwtToken } = await isHuman(token)
288
303
  if (human) {
289
- toast.success("You are a human! 👌🏻")
304
+ toast.success(t("stepWizard.humanSuccess"))
290
305
 
291
306
  console.log("JWT TOKEN received", jwtToken)
292
307
  setAuth({
@@ -307,14 +322,14 @@ function App() {
307
322
  ),
308
323
  },
309
324
  {
310
- title: "Any materials to get this course started?",
325
+ title: t("stepWizard.hasContentIndex"),
311
326
  slug: "hasContentIndex",
312
327
  isCompleted: false,
313
328
  content: (
314
329
  <>
315
330
  <div className="flex flex-col md:flex-row gap-2 justify-center">
316
331
  <SelectableCard
317
- title="❌ No, help me create one"
332
+ title={t("stepWizard.hasContentIndexCard.no")}
318
333
  onClick={() => {
319
334
  setFormState({
320
335
  hasContentIndex: false,
@@ -329,7 +344,7 @@ function App() {
329
344
  // selected={formState.hasContentIndex === false}
330
345
  />
331
346
  <SelectableCard
332
- title="✅ Yes"
347
+ title={t("stepWizard.hasContentIndexCard.yes")}
333
348
  onClick={() => {
334
349
  setFormState({
335
350
  hasContentIndex: true,
@@ -344,10 +359,9 @@ function App() {
344
359
  ),
345
360
  },
346
361
  {
347
- title: "Any materials to get this course started?",
362
+ title: t("stepWizard.contentIndex"),
348
363
  slug: "contentIndex",
349
- helpText:
350
- "It could be just text, paste an URL or even upload a document.",
364
+ helpText: t("stepWizard.contentIndexHelpText"),
351
365
  isCompleted:
352
366
  formState.contentIndex.length > 0 || uploadedFiles.length > 0,
353
367
  required: true,
@@ -376,11 +390,23 @@ function App() {
376
390
  <ParamsChecker />
377
391
  {formState.isCompleted && history.length === 0 ? (
378
392
  <Loader
379
- text="Learnpack is setting up your tutorial. It may take a moment..."
393
+ text={t("loader.text")}
380
394
  icon={<img src={RIGO_FLOAT_GIF} alt="rigo" className="w-20 h-20" />}
381
395
  />
382
396
  ) : (
383
397
  <>
398
+ {/* {formState.language && (
399
+ <div className="flex flex-col items-center justify-center">
400
+ <p>
401
+ <span className="font-bold">Language:</span>{" "}
402
+ {formState.language}
403
+ </p>
404
+ <p>
405
+ <span className="font-bold">Technologies:</span>{" "}
406
+ {formState.technologies?.join(", ")}
407
+ </p>
408
+ </div>
409
+ )} */}
384
410
  {history.length > 0 && (
385
411
  <ResumeCourseModal
386
412
  onContinue={() => {
@@ -6,7 +6,7 @@ import toast from "react-hot-toast"
6
6
  import CreatorSocket from "../utils/socket"
7
7
  import { DEV_MODE, RIGOBOT_HOST } from "../utils/constants"
8
8
  import axios from "axios"
9
-
9
+ import { useTranslation } from "react-i18next"
10
10
 
11
11
  // `
12
12
  // app.post("/read-document", upload.single("file"), async (req, res) => {
@@ -174,6 +174,7 @@ const FileUploader: React.FC<FileUploaderProps> = ({
174
174
  onFinish,
175
175
  }) => {
176
176
  // const rigoToken = useStore((state) => state.auth.rigoToken)
177
+ const { t } = useTranslation()
177
178
  const publicToken = useStore((state) => state.auth.publicToken)
178
179
  const inputRef = useRef<HTMLInputElement>(null)
179
180
  const uploadedFiles = useStore((state) => state.uploadedFiles)
@@ -293,10 +294,10 @@ const FileUploader: React.FC<FileUploaderProps> = ({
293
294
  <ContentCard
294
295
  description={
295
296
  isDragging
296
- ? "Drop it here"
297
+ ? t("uploader.files.drop")
297
298
  : isLoading
298
- ? "Processing..."
299
- : "Upload a PDF or DOCX file or drag it here"
299
+ ? t("uploader.files.processing")
300
+ : t("uploader.files.descriptionLong")
300
301
  }
301
302
  icon={isDragging ? SVGS.clip : SVGS.pdf}
302
303
  onClick={() => inputRef.current?.click()}
@@ -315,7 +316,7 @@ const FileUploader: React.FC<FileUploaderProps> = ({
315
316
  onFinish?.(uploadedFiles)
316
317
  }}
317
318
  >
318
- 🚀 Finish
319
+ {t("uploader.files.finish")}
319
320
  </button>
320
321
  </>
321
322
  )}
@@ -1,4 +1,5 @@
1
1
  import React, { useState } from "react"
2
+ import { useTranslation } from "react-i18next"
2
3
 
3
4
  export interface ParsedLink {
4
5
  url: string
@@ -19,6 +20,7 @@ const toBase64Url = (str: string) =>
19
20
  btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")
20
21
 
21
22
  const LinkUploader: React.FC<LinkUploaderProps> = ({ onResult }) => {
23
+ const { t } = useTranslation()
22
24
  const [url, setUrl] = useState("")
23
25
  const [loading, setLoading] = useState(false)
24
26
  const [error, setError] = useState<string | null>(null)
@@ -54,7 +56,7 @@ const LinkUploader: React.FC<LinkUploaderProps> = ({ onResult }) => {
54
56
  onChange={(e) => setUrl(e.target.value)}
55
57
  onKeyDown={(e) => e.key === "Enter" && handleAdd()}
56
58
  disabled={loading}
57
- placeholder="Paste your link here…"
59
+ placeholder={t("uploader.youtube.placeholder")}
58
60
  className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-200 transition disabled:bg-gray-100"
59
61
  />
60
62
  <button
@@ -5,9 +5,12 @@ import toast from "react-hot-toast"
5
5
  import useStore from "../utils/store"
6
6
  import { useShallow } from "zustand/react/shallow"
7
7
  import { login4Geeks, registerUserWithFormData } from "../utils/lib"
8
+ import { useTranslation } from "react-i18next"
8
9
 
9
10
  export default function Login({ onFinish }: { onFinish: () => void }) {
10
11
  // Login states
12
+ const { t } = useTranslation()
13
+
11
14
  const [email, setEmail] = useState("")
12
15
  const [password, setPassword] = useState("")
13
16
  const [isLoading, setIsLoading] = useState(false)
@@ -28,20 +31,20 @@ export default function Login({ onFinish }: { onFinish: () => void }) {
28
31
  const login = async (e: React.FormEvent<HTMLFormElement>) => {
29
32
  e.preventDefault()
30
33
  setIsLoading(true)
31
- const tid = toast.loading("Logging in…")
34
+ const tid = toast.loading(t("login.loggingIn"))
32
35
  try {
33
36
  if (!email || !password) {
34
37
  setIsLoading(false)
35
- toast.error("Please fill all fields", { id: tid })
38
+ toast.error(t("login.pleaseFillAllFields"), { id: tid })
36
39
  return
37
40
  }
38
41
  const resp = await login4Geeks({ email, password })
39
42
  if (!resp) {
40
43
  setIsLoading(false)
41
- toast.error("Invalid credentials", { id: tid })
44
+ toast.error(t("login.invalidCredentials"), { id: tid })
42
45
  return
43
46
  }
44
- toast.success("Logged in successfully", { id: tid })
47
+ toast.success(t("login.loggedInSuccessfully"), { id: tid })
45
48
  setAuth({
46
49
  bcToken: resp.token,
47
50
  userId: resp.user.id,
@@ -62,21 +65,21 @@ export default function Login({ onFinish }: { onFinish: () => void }) {
62
65
  const handleSignup = async (e: React.FormEvent<HTMLFormElement>) => {
63
66
  e.preventDefault()
64
67
  setIsLoading(true)
65
- const tid = toast.loading("Creating account…")
68
+ const tid = toast.loading(t("login.creatingAccount"))
66
69
  const { firstName, lastName, email } = signupData
67
70
  if (!firstName || !lastName || !email) {
68
71
  setIsLoading(false)
69
- toast.error("Please fill all fields", { id: tid })
72
+ toast.error(t("login.pleaseFillAllFields"), { id: tid })
70
73
  return
71
74
  }
72
75
  try {
73
76
  await registerUserWithFormData(firstName, lastName, email)
74
- toast.success("Account created! Check your email.", { id: tid })
77
+ toast.success(t("login.accountCreated"), { id: tid })
75
78
  setShowSignup(false)
76
79
  setShowForm(true)
77
80
  setEmail(email)
78
81
  } catch (err) {
79
- toast.error("Registration failed. Try again.", { id: tid })
82
+ toast.error(t("login.registrationFailed"), { id: tid })
80
83
  } finally {
81
84
  setIsLoading(false)
82
85
  }
@@ -109,12 +112,12 @@ export default function Login({ onFinish }: { onFinish: () => void }) {
109
112
  {showSignup ? (
110
113
  <>
111
114
  <h2 className="mb-4 text-xl font-semibold text-center">
112
- Create your account
115
+ {t("login.createYourAccount")}
113
116
  </h2>
114
117
  <form className="space-y-3" onSubmit={handleSignup}>
115
118
  <input
116
119
  type="text"
117
- placeholder="First name"
120
+ placeholder={t("login.firstName")}
118
121
  className="w-full border border-gray-300 px-3 py-2 rounded-md"
119
122
  value={signupData.firstName}
120
123
  onChange={(e) =>
@@ -123,7 +126,7 @@ export default function Login({ onFinish }: { onFinish: () => void }) {
123
126
  />
124
127
  <input
125
128
  type="text"
126
- placeholder="Last name"
129
+ placeholder={t("login.lastName")}
127
130
  className="w-full border border-gray-300 px-3 py-2 rounded-md"
128
131
  value={signupData.lastName}
129
132
  onChange={(e) =>
@@ -144,11 +147,11 @@ export default function Login({ onFinish }: { onFinish: () => void }) {
144
147
  className="w-full bg-blue-600 text-white py-2 rounded-md font-semibold cursor-pointer"
145
148
  disabled={isLoading}
146
149
  >
147
- {isLoading ? "Creating..." : "Sign up"}
150
+ {isLoading ? t("login.creating") : t("login.signUp")}
148
151
  </button>
149
152
  </form>
150
153
  <div className="mt-4 text-sm text-center">
151
- Already have an account?{" "}
154
+ {t("login.alreadyHaveAnAccount")}
152
155
  <button
153
156
  className="text-blue-600 font-medium"
154
157
  onClick={() => {
@@ -156,31 +159,32 @@ export default function Login({ onFinish }: { onFinish: () => void }) {
156
159
  setShowForm(true)
157
160
  }}
158
161
  >
159
- Log in here
162
+ {t("login.logInHere")}
160
163
  </button>
161
164
  </div>
162
165
  </>
163
166
  ) : (
164
167
  <>
165
168
  <p className="mb-4 text-center text-gray-700">
166
- You need to have a 4Geeks account with a creator plan to create a
167
- tutorial.
169
+ {t("login.youNeedToHaveAnAccount")}
168
170
  </p>
169
171
  <button
170
172
  onClick={redirectGithub}
171
173
  className="w-full border border-gray-300 py-2 rounded-md font-semibold flex items-center justify-center gap-2 mb-4 cursor-pointer"
172
174
  >
173
- {SVGS.github} LOGIN WITH GITHUB
175
+ {SVGS.github} {t("login.loginWithGithub")}
174
176
  </button>
175
177
  <button
176
178
  onClick={redirectGoogle}
177
179
  className="w-full border border-gray-300 py-2 rounded-md font-semibold flex items-center justify-center gap-2 mb-4 cursor-pointer"
178
180
  >
179
- {SVGS.google} LOGIN WITH GOOGLE
181
+ {SVGS.google} {t("login.loginWithGoogle")}
180
182
  </button>
181
183
  <div className="flex items-center mb-4">
182
184
  <hr className="flex-grow border-gray-300" />
183
- <span className="mx-2 text-gray-400 text-sm">or</span>
185
+ <span className="mx-2 text-gray-400 text-sm">
186
+ {t("login.or")}
187
+ </span>
184
188
  <hr className="flex-grow border-gray-300" />
185
189
  </div>
186
190
  {showForm ? (
@@ -194,7 +198,7 @@ export default function Login({ onFinish }: { onFinish: () => void }) {
194
198
  />
195
199
  <input
196
200
  type="password"
197
- placeholder="Password"
201
+ placeholder={t("login.password")}
198
202
  className="w-full border border-gray-300 px-3 py-2 rounded-md"
199
203
  value={password}
200
204
  onChange={(e) => setPassword(e.target.value)}
@@ -204,23 +208,23 @@ export default function Login({ onFinish }: { onFinish: () => void }) {
204
208
  type="submit"
205
209
  className="flex-1 bg-blue-500 text-white py-2 rounded-md font-semibold cursor-pointer"
206
210
  >
207
- {isLoading ? "Logging in..." : "Log in"}
211
+ {isLoading ? t("login.loggingIn") : t("login.logIn")}
208
212
  </button>
209
213
  <button
210
214
  type="button"
211
215
  onClick={() => setShowForm(false)}
212
216
  className="flex-1 border border-blue-500 text-blue-600 py-2 rounded-md font-semibold cursor-pointer"
213
217
  >
214
- Skip
218
+ {t("login.skip")}
215
219
  </button>
216
220
  </div>
217
221
  <div className="text-sm text-gray-600 mt-2">
218
- Forgot your password?{" "}
222
+ {t("login.forgotPassword")}
219
223
  <a
220
224
  href={`${BREATHECODE_HOST}/v1/auth/password/reset?url=${getCurrentUrlWithQueryParams()}`}
221
225
  className="text-blue-600 font-medium"
222
226
  >
223
- Recover it here.
227
+ {t("login.recoverItHere")}
224
228
  </a>
225
229
  </div>
226
230
  </form>
@@ -229,12 +233,14 @@ export default function Login({ onFinish }: { onFinish: () => void }) {
229
233
  onClick={() => setShowForm(true)}
230
234
  className="w-full bg-blue-600 text-white py-2 rounded-md font-semibold cursor-pointer"
231
235
  >
232
- Login with Email
236
+ {t("login.loginWithEmail")}
233
237
  </button>
234
238
  )}
235
239
  <div className="flex justify-between items-center mt-4">
236
240
  <div className="bg-blue-50 text-sm p-2 rounded text-left">
237
- <p className="text-gray-700 m-0">You don't have an account?</p>
241
+ <p className="text-gray-700 m-0">
242
+ {t("login.youDontHaveAnAccount")}
243
+ </p>
238
244
  {/* <button
239
245
  className="text-blue-600 font-medium cursor-pointer"
240
246
  onClick={() => setShowSignup(true)}
@@ -246,7 +252,7 @@ export default function Login({ onFinish }: { onFinish: () => void }) {
246
252
  target="_blank"
247
253
  className="text-blue-600 font-medium"
248
254
  >
249
- Register here.
255
+ {t("login.registerHere")}
250
256
  </a>
251
257
  </div>
252
258
  </div>
@@ -1,6 +1,7 @@
1
1
  import React from "react"
2
2
  import SelectableCard from "./SelectableCard"
3
3
  import useStore from "../utils/store"
4
+ import { useTranslation } from "react-i18next"
4
5
 
5
6
  export const possiblePurposes = [
6
7
  "learnpack-lesson-writer",
@@ -15,31 +16,6 @@ type PurposeSlug =
15
16
  | "skill-building-facilitator"
16
17
  | "certification-preparation-specialist"
17
18
 
18
- const PURPOSES: { slug: PurposeSlug; label: string; description: string }[] = [
19
- {
20
- slug: "learnpack-lesson-writer",
21
- label: "Understand a new topic",
22
- description:
23
- "Learn about a new concept (e.g., ISO 27001 or exponential decay).",
24
- },
25
- {
26
- slug: "homework-and-exam-preparation-aid",
27
- label: "Practice for an exam or homework",
28
- description: "Solve math problems or certification questions.",
29
- },
30
- {
31
- slug: "skill-building-facilitator",
32
- label: "Build real-world skills",
33
- description:
34
- "Apply concepts to projects, data science, or security audits.",
35
- },
36
- {
37
- slug: "certification-preparation-specialist",
38
- label: "Prepare for a certification",
39
- description: "Get ready for exams like ISO 27001 Lead Auditor.",
40
- },
41
- ]
42
-
43
19
  interface PurposeSelectorProps {
44
20
  onFinish: (purpose: PurposeSlug) => void
45
21
  // Optionally allow passing an initial purpose, for testability or server-side rendering
@@ -50,6 +26,37 @@ export const PurposeSelector: React.FC<PurposeSelectorProps> = ({
50
26
  onFinish,
51
27
  }) => {
52
28
  const formState = useStore((state) => state.formState)
29
+ const { t } = useTranslation()
30
+
31
+ const PURPOSES: { slug: PurposeSlug; label: string; description: string }[] =
32
+ [
33
+ {
34
+ slug: "learnpack-lesson-writer",
35
+ label: t("purposeSelector.learnpack-lesson-writer.label"),
36
+ description: t("purposeSelector.learnpack-lesson-writer.description"),
37
+ },
38
+ {
39
+ slug: "homework-and-exam-preparation-aid",
40
+ label: t("purposeSelector.homework-and-exam-preparation-aid.label"),
41
+ description: t(
42
+ "purposeSelector.homework-and-exam-preparation-aid.description"
43
+ ),
44
+ },
45
+ {
46
+ slug: "skill-building-facilitator",
47
+ label: t("purposeSelector.skill-building-facilitator.label"),
48
+ description: t(
49
+ "purposeSelector.skill-building-facilitator.description"
50
+ ),
51
+ },
52
+ {
53
+ slug: "certification-preparation-specialist",
54
+ label: t("purposeSelector.certification-preparation-specialist.label"),
55
+ description: t(
56
+ "purposeSelector.certification-preparation-specialist.description"
57
+ ),
58
+ },
59
+ ]
53
60
 
54
61
  return (
55
62
  <div className="w-full">
@@ -1,6 +1,7 @@
1
1
  import React from "react"
2
2
  import { SVGS } from "../assets/svgs"
3
3
  import { toast } from "react-hot-toast"
4
+ import { useTranslation } from "react-i18next"
4
5
 
5
6
  export type Step = {
6
7
  title: string
@@ -27,6 +28,7 @@ const StepWizard: React.FC<Props> = ({
27
28
  onFinish,
28
29
  hideLastButton = false,
29
30
  }) => {
31
+ const { t } = useTranslation()
30
32
  const currentStep = formState.currentStep
31
33
  const index = steps.findIndex((step) => step.slug === currentStep)
32
34
  const totalSteps = steps.length
@@ -34,7 +36,7 @@ const StepWizard: React.FC<Props> = ({
34
36
  const goNext = () => {
35
37
  const index = steps.findIndex((step) => step.slug === currentStep)
36
38
  if (steps[index].required && !steps[index].isCompleted) {
37
- toast.error("Please fill out all required fields!")
39
+ toast.error(t("stepWizard.requiredFields"))
38
40
  return
39
41
  }
40
42
  if (index < totalSteps - 1)
@@ -55,7 +57,7 @@ const StepWizard: React.FC<Props> = ({
55
57
  <div className=" rounded-xl p-8 w-full max-w-xl">
56
58
  <h5 className="text-sm text-blue-400 uppercase mb-2">
57
59
  {steps.find((step) => step.slug === currentStep)?.subtitle ||
58
- "Setting up your tutorial"}
60
+ t("stepWizard.subtitle")}
59
61
  </h5>
60
62
  <h2 className="text-lg font-medium mb-6">
61
63
  <span className="text-blue-600 mr-1">{index + 1}.</span>
@@ -97,7 +99,7 @@ const StepWizard: React.FC<Props> = ({
97
99
  index === 0 ? "cursor-not-allowed" : "cursor-pointer"
98
100
  }`}
99
101
  >
100
- Back
102
+ {t("stepWizard.back")}
101
103
  </button>
102
104
  {!(hideLastButton && index === totalSteps - 1) && (
103
105
  <button
@@ -106,7 +108,9 @@ const StepWizard: React.FC<Props> = ({
106
108
  index === totalSteps - 1 ? "bg-blue-600 text-white" : ""
107
109
  }`}
108
110
  >
109
- {index === totalSteps - 1 ? "Finish 🚀" : "Next ➡"}
111
+ {index === totalSteps - 1
112
+ ? t("stepWizard.finish")
113
+ : t("stepWizard.next")}
110
114
  </button>
111
115
  )}
112
116
  </div>
@@ -3,15 +3,17 @@ import { SVGS } from "../assets/svgs"
3
3
  import { ContentCard } from "./ContentCard"
4
4
  import LinkUploader from "./LinkUploader"
5
5
  import FileUploader from "./FileUploader"
6
+ import { useTranslation } from "react-i18next"
6
7
 
7
8
  const TextUploader = ({ onFinish }: { onFinish: (text: string) => void }) => {
8
9
  const [text, setText] = useState("")
10
+ const { t } = useTranslation()
9
11
 
10
12
  return (
11
13
  <div className="flex flex-col gap-2 w-full">
12
14
  <textarea
13
15
  className="mt-4 p-2 border rounded w-full bg-white border-heavy-blue"
14
- placeholder="Add your text here..."
16
+ placeholder={t("uploader.text.placeholder")}
15
17
  value={text}
16
18
  onChange={(e) => setText(e.target.value)}
17
19
  />
@@ -19,7 +21,7 @@ const TextUploader = ({ onFinish }: { onFinish: (text: string) => void }) => {
19
21
  className="mt-4 p-2 border rounded w-full bg-learnpack text-white"
20
22
  onClick={() => onFinish(text)}
21
23
  >
22
- Finish
24
+ {t("uploader.text.finish")}
23
25
  </button>
24
26
  </div>
25
27
  )
@@ -31,6 +33,7 @@ export const Uploader = ({
31
33
  onFinish: (text: string) => void
32
34
  }) => {
33
35
  const [selectedOption, setSelectedOption] = useState<string | null>(null)
36
+ const { t } = useTranslation()
34
37
 
35
38
  const handleSelectOption = (option: string) => {
36
39
  setSelectedOption(option)
@@ -41,18 +44,18 @@ export const Uploader = ({
41
44
  {!selectedOption && (
42
45
  <>
43
46
  <ContentCard
44
- description="Write or paste a content table"
47
+ description={t("uploader.text.description")}
45
48
  icon={SVGS.contentTable}
46
49
  onClick={() => handleSelectOption("text")}
47
50
  />
48
51
  <ContentCard
49
- description="Upload files"
52
+ description={t("uploader.files.description")}
50
53
  icon={SVGS.pdf}
51
54
  onClick={() => handleSelectOption("files")}
52
55
  />
53
56
 
54
57
  <ContentCard
55
- description="Share a Youtube link"
58
+ description={t("uploader.youtube.description")}
56
59
  icon={SVGS.youtube}
57
60
  onClick={() => handleSelectOption("youtube")}
58
61
  />