@learnpack/learnpack 5.0.196 → 5.0.202

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 (40) hide show
  1. package/README.md +13 -13
  2. package/lib/commands/serve.d.ts +5 -28
  3. package/lib/commands/serve.js +45 -20
  4. package/lib/creatorDist/assets/index-C_HbkVCg.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 +59 -59
  12. package/src/creator/package-lock.json +97 -1
  13. package/src/creator/package.json +3 -0
  14. package/src/creator/src/App.tsx +91 -32
  15. package/src/creator/src/components/FileCard.tsx +2 -2
  16. package/src/creator/src/components/FileUploader.tsx +6 -5
  17. package/src/creator/src/components/LinkUploader.tsx +3 -1
  18. package/src/creator/src/components/Login.tsx +33 -27
  19. package/src/creator/src/components/PurposeSelector.tsx +32 -25
  20. package/src/creator/src/components/StepWizard.tsx +8 -4
  21. package/src/creator/src/components/Uploader.tsx +8 -5
  22. package/src/creator/src/components/syllabus/ContentIndex.tsx +17 -11
  23. package/src/creator/src/components/syllabus/Sidebar.tsx +7 -7
  24. package/src/creator/src/components/syllabus/SyllabusEditor.tsx +79 -76
  25. package/src/creator/src/i18n.ts +28 -0
  26. package/src/creator/src/locales/en.json +110 -0
  27. package/src/creator/src/locales/es.json +110 -0
  28. package/src/creator/src/main.tsx +1 -0
  29. package/src/creator/src/utils/creatorUtils.ts +7 -3
  30. package/src/creator/src/utils/lib.ts +17 -1
  31. package/src/creator/src/utils/store.ts +37 -10
  32. package/src/creatorDist/assets/index-C_HbkVCg.js +38491 -0
  33. package/src/creatorDist/index.html +1 -1
  34. package/src/models/creator.ts +32 -0
  35. package/src/ui/_app/app.css +1 -1
  36. package/src/ui/_app/app.js +55 -55
  37. package/src/ui/app.tar.gz +0 -0
  38. package/src/utils/creatorUtilities.ts +4 -4
  39. package/lib/creatorDist/assets/index-CXaPa6wN.js +0 -35382
  40. package/src/creatorDist/assets/index-CXaPa6wN.js +0 -35382
@@ -7,7 +7,13 @@ import { useShallow } from "zustand/react/shallow"
7
7
  import useStore from "./utils/store"
8
8
 
9
9
  import { publicInteractiveCreation, isHuman } from "./utils/rigo"
10
- import { checkParams, loginWithToken, parseLesson } from "./utils/lib"
10
+ import {
11
+ checkParams,
12
+ isValidRigoToken,
13
+ loginWithToken,
14
+ parseLesson,
15
+ fixTitleLength,
16
+ } from "./utils/lib"
11
17
 
12
18
  import { Uploader } from "./components/Uploader"
13
19
  import toast from "react-hot-toast"
@@ -17,9 +23,11 @@ import TurnstileChallenge from "./components/TurnstileChallenge"
17
23
  // import TurnstileChallenge from "./components/TurnstileChallenge"
18
24
  import ResumeCourseModal from "./components/ResumeCourseModal"
19
25
  import { possiblePurposes, PurposeSelector } from "./components/PurposeSelector"
26
+ import { useTranslation } from "react-i18next"
20
27
 
21
28
  function App() {
22
29
  const navigate = useNavigate()
30
+ const { t, i18n } = useTranslation()
23
31
 
24
32
  const {
25
33
  formState,
@@ -32,6 +40,8 @@ function App() {
32
40
  uploadedFiles,
33
41
  auth,
34
42
  resetFormState,
43
+ cleanAll,
44
+ setMessages,
35
45
  } = useStore(
36
46
  useShallow((state) => ({
37
47
  formState: state.formState,
@@ -44,6 +54,8 @@ function App() {
44
54
  uploadedFiles: state.uploadedFiles,
45
55
  auth: state.auth,
46
56
  resetFormState: state.resetFormState,
57
+ cleanAll: state.cleanAll,
58
+ setMessages: state.setMessages,
47
59
  }))
48
60
  )
49
61
 
@@ -73,11 +85,12 @@ function App() {
73
85
  }
74
86
 
75
87
  const checkDescription = () => {
76
- const { description, duration, plan, purpose } = checkParams([
88
+ const { description, duration, plan, purpose, language } = checkParams([
77
89
  "description",
78
90
  "duration",
79
91
  "plan",
80
92
  "purpose",
93
+ "language",
81
94
  ])
82
95
  if (description) {
83
96
  console.log("description", description)
@@ -105,6 +118,12 @@ function App() {
105
118
  console.debug("No plan received in params")
106
119
  }
107
120
 
121
+ if (language && language.length === 2) {
122
+ setFormState({
123
+ language: language,
124
+ })
125
+ }
126
+
108
127
  if (purpose && purpose.length > 0 && possiblePurposes.includes(purpose)) {
109
128
  setFormState({
110
129
  purpose: purpose,
@@ -115,42 +134,69 @@ function App() {
115
134
 
116
135
  const handleCreateTutorial = async () => {
117
136
  try {
118
- cleanHistory()
137
+ const isValid = await isValidRigoToken(auth.rigoToken)
138
+ if (!isValid) {
139
+ setAuth({
140
+ ...auth,
141
+ rigoToken: "",
142
+ bcToken: "",
143
+ userId: "",
144
+ user: null,
145
+ })
146
+ }
119
147
 
120
148
  const res = await publicInteractiveCreation(
121
149
  {
122
- courseInfo: `${JSON.stringify(formState)} `,
123
- prevInteractions: "",
150
+ courseInfo: `${JSON.stringify(formState)}`,
151
+ prevInteractions: "USER: " + formState.description,
124
152
  },
125
- auth.rigoToken ? auth.rigoToken : auth.publicToken,
153
+ auth.rigoToken && isValid ? auth.rigoToken : auth.publicToken,
126
154
  formState.purpose || "learnpack-lesson-writer",
127
- auth.rigoToken ? false : true
155
+ auth.rigoToken && isValid ? false : true
128
156
  )
129
157
  const lessons = res.parsed.listOfSteps.map((lesson: any) => {
130
158
  return parseLesson(lesson, [])
131
159
  })
132
160
 
133
- console.log("RES FROM RIGO", res)
134
161
  push({
135
162
  lessons,
136
163
  courseInfo: {
137
164
  ...formState,
138
- title: res.parsed.title,
165
+ title: fixTitleLength(res.parsed.title),
139
166
  description: res.parsed.description,
167
+ language: res.parsed.languageCode || formState.language || "en",
168
+ technologies: res.parsed.technologies,
140
169
  },
141
- messages: [
142
- {
143
- type: "assistant",
144
- content: res.parsed.aiMessage,
145
- },
146
- ],
147
- // sources: uploadedFiles,
148
170
  })
171
+
172
+ if (res.parsed.languageCode) {
173
+ i18n.changeLanguage(res.parsed.languageCode)
174
+ }
175
+
149
176
  navigate("/creator/syllabus")
150
177
  setFormState({
151
178
  isCompleted: false,
152
179
  currentStep: "description",
153
180
  })
181
+ setMessages([
182
+ {
183
+ type: "user",
184
+ content: formState.description,
185
+ },
186
+ {
187
+ type: "assistant",
188
+ content: res.parsed.aiMessage,
189
+ },
190
+ {
191
+ type: "assistant",
192
+ content: "If you're satisfied, type 'OK' in the chat.",
193
+ },
194
+ {
195
+ type: "assistant",
196
+ content:
197
+ "If not, what would you like me to change? You can sat things like:\n - Add more exercises\n - Make it more difficult\n - Remove step 1.1 and replace it with a new step that explains the concept of X ",
198
+ },
199
+ ])
154
200
  } catch (error) {
155
201
  console.error(error, "ERROR CREATING TUTORIAL")
156
202
  toast.error("Something went wrong. Please try again.")
@@ -164,14 +210,14 @@ function App() {
164
210
  const buildSteps = () => {
165
211
  const steps = [
166
212
  {
167
- title: "What do you want to learn?",
213
+ title: t("stepWizard.description"),
168
214
  slug: "description",
169
215
  isCompleted: formState.description.length > 0,
170
216
  required: true,
171
217
  content: (
172
218
  <textarea
173
219
  required
174
- placeholder="Describe your course"
220
+ placeholder={t("stepWizard.descriptionPlaceholder")}
175
221
  className="w-full h-24 border-2 border-gray-300 rounded-md p-2 bg-white"
176
222
  value={formState.description}
177
223
  onChange={(e) => {
@@ -184,14 +230,14 @@ function App() {
184
230
  },
185
231
 
186
232
  {
187
- title: "What is the estimated duration for this tutorial?",
233
+ title: t("stepWizard.duration"),
188
234
  slug: "duration",
189
235
  isCompleted: formState.duration > 0,
190
236
  required: true,
191
237
  content: (
192
238
  <div className="flex flex-col md:flex-row gap-2">
193
239
  <SelectableCard
194
- title="Around 30 minutes"
240
+ title={t("stepWizard.durationCard.30")}
195
241
  // subtitle="This is a tutorial that will take 30 minutes to complete"
196
242
  onClick={() => {
197
243
  setFormState({
@@ -202,7 +248,7 @@ function App() {
202
248
  selected={formState.duration === 30}
203
249
  />
204
250
  <SelectableCard
205
- title="Around 1 hour"
251
+ title={t("stepWizard.durationCard.60")}
206
252
  // subtitle="This is a tutorial that will take 1 hour to complete"
207
253
  onClick={() => {
208
254
  setFormState({
@@ -213,7 +259,7 @@ function App() {
213
259
  selected={formState.duration === 60}
214
260
  />
215
261
  <SelectableCard
216
- title="Around 2 hours"
262
+ title={t("stepWizard.durationCard.120")}
217
263
  // subtitle="This is a tutorial that will take 2 hours to complete"
218
264
  onClick={() => {
219
265
  setFormState({
@@ -227,7 +273,7 @@ function App() {
227
273
  ),
228
274
  },
229
275
  {
230
- title: "How would you like to use learnpack?",
276
+ title: t("stepWizard.purpose"),
231
277
  slug: "purpose",
232
278
  isCompleted: formState?.purpose?.length > 0,
233
279
  required: true,
@@ -243,9 +289,10 @@ function App() {
243
289
  ),
244
290
  },
245
291
  {
246
- title: "Please verify you are a human",
292
+ title: t("stepWizard.verifyHuman"),
247
293
  slug: "verifyHuman",
248
294
  isCompleted: false,
295
+ required: true,
249
296
  content: (
250
297
  <TurnstileChallenge
251
298
  siteKey={
@@ -254,7 +301,7 @@ function App() {
254
301
  onSuccess={async (token) => {
255
302
  const { human, message, token: jwtToken } = await isHuman(token)
256
303
  if (human) {
257
- toast.success("You are a human! 👌🏻")
304
+ toast.success(t("stepWizard.humanSuccess"))
258
305
 
259
306
  console.log("JWT TOKEN received", jwtToken)
260
307
  setAuth({
@@ -275,14 +322,14 @@ function App() {
275
322
  ),
276
323
  },
277
324
  {
278
- title: "Any materials to get this course started?",
325
+ title: t("stepWizard.hasContentIndex"),
279
326
  slug: "hasContentIndex",
280
327
  isCompleted: false,
281
328
  content: (
282
329
  <>
283
330
  <div className="flex flex-col md:flex-row gap-2 justify-center">
284
331
  <SelectableCard
285
- title="❌ No, help me create one"
332
+ title={t("stepWizard.hasContentIndexCard.no")}
286
333
  onClick={() => {
287
334
  setFormState({
288
335
  hasContentIndex: false,
@@ -297,7 +344,7 @@ function App() {
297
344
  // selected={formState.hasContentIndex === false}
298
345
  />
299
346
  <SelectableCard
300
- title="✅ Yes"
347
+ title={t("stepWizard.hasContentIndexCard.yes")}
301
348
  onClick={() => {
302
349
  setFormState({
303
350
  hasContentIndex: true,
@@ -312,10 +359,9 @@ function App() {
312
359
  ),
313
360
  },
314
361
  {
315
- title: "Any materials to get this course started?",
362
+ title: t("stepWizard.contentIndex"),
316
363
  slug: "contentIndex",
317
- helpText:
318
- "It could be just text, paste an URL or even upload a document.",
364
+ helpText: t("stepWizard.contentIndexHelpText"),
319
365
  isCompleted:
320
366
  formState.contentIndex.length > 0 || uploadedFiles.length > 0,
321
367
  required: true,
@@ -344,11 +390,23 @@ function App() {
344
390
  <ParamsChecker />
345
391
  {formState.isCompleted && history.length === 0 ? (
346
392
  <Loader
347
- text="Learnpack is setting up your tutorial. It may take a moment..."
393
+ text={t("loader.text")}
348
394
  icon={<img src={RIGO_FLOAT_GIF} alt="rigo" className="w-20 h-20" />}
349
395
  />
350
396
  ) : (
351
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
+ )} */}
352
410
  {history.length > 0 && (
353
411
  <ResumeCourseModal
354
412
  onContinue={() => {
@@ -356,6 +414,7 @@ function App() {
356
414
  }}
357
415
  onStartOver={() => {
358
416
  resetFormState()
417
+ cleanAll()
359
418
  cleanHistory()
360
419
  }}
361
420
  />
@@ -1,11 +1,11 @@
1
1
  import { SVGS } from "../assets/svgs"
2
- import { UploadedFile } from "../utils/store"
2
+ import { ParsedFile } from "./FileUploader"
3
3
 
4
4
  export const FileCard = ({
5
5
  file,
6
6
  handleRemove,
7
7
  }: {
8
- file: UploadedFile
8
+ file: ParsedFile
9
9
  handleRemove: () => void
10
10
  }) => (
11
11
  <div className="flex items-center justify-between bg-white shadow-sm p-2 rounded-lg w-[100px]">
@@ -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">