@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.
- package/README.md +13 -13
- package/lib/commands/serve.d.ts +5 -28
- package/lib/commands/serve.js +45 -20
- package/lib/creatorDist/assets/index-C_HbkVCg.js +38491 -0
- package/lib/creatorDist/index.html +1 -1
- package/lib/models/creator.d.ts +30 -0
- package/lib/models/creator.js +2 -0
- package/lib/utils/creatorUtilities.js +3 -2
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
- package/src/commands/serve.ts +59 -59
- package/src/creator/package-lock.json +97 -1
- package/src/creator/package.json +3 -0
- package/src/creator/src/App.tsx +91 -32
- package/src/creator/src/components/FileCard.tsx +2 -2
- package/src/creator/src/components/FileUploader.tsx +6 -5
- package/src/creator/src/components/LinkUploader.tsx +3 -1
- package/src/creator/src/components/Login.tsx +33 -27
- package/src/creator/src/components/PurposeSelector.tsx +32 -25
- package/src/creator/src/components/StepWizard.tsx +8 -4
- package/src/creator/src/components/Uploader.tsx +8 -5
- package/src/creator/src/components/syllabus/ContentIndex.tsx +17 -11
- package/src/creator/src/components/syllabus/Sidebar.tsx +7 -7
- package/src/creator/src/components/syllabus/SyllabusEditor.tsx +79 -76
- package/src/creator/src/i18n.ts +28 -0
- package/src/creator/src/locales/en.json +110 -0
- package/src/creator/src/locales/es.json +110 -0
- package/src/creator/src/main.tsx +1 -0
- package/src/creator/src/utils/creatorUtils.ts +7 -3
- package/src/creator/src/utils/lib.ts +17 -1
- package/src/creator/src/utils/store.ts +37 -10
- package/src/creatorDist/assets/index-C_HbkVCg.js +38491 -0
- package/src/creatorDist/index.html +1 -1
- package/src/models/creator.ts +32 -0
- package/src/ui/_app/app.css +1 -1
- package/src/ui/_app/app.js +55 -55
- package/src/ui/app.tar.gz +0 -0
- package/src/utils/creatorUtilities.ts +4 -4
- package/lib/creatorDist/assets/index-CXaPa6wN.js +0 -35382
- package/src/creatorDist/assets/index-CXaPa6wN.js +0 -35382
package/src/creator/src/App.tsx
CHANGED
@@ -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 {
|
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
|
-
|
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: "
|
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="
|
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: "
|
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="
|
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="
|
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="
|
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: "
|
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: "
|
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("
|
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: "
|
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="
|
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="
|
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: "
|
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="
|
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 {
|
2
|
+
import { ParsedFile } from "./FileUploader"
|
3
3
|
|
4
4
|
export const FileCard = ({
|
5
5
|
file,
|
6
6
|
handleRemove,
|
7
7
|
}: {
|
8
|
-
file:
|
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
|
-
? "
|
297
|
+
? t("uploader.files.drop")
|
297
298
|
: isLoading
|
298
|
-
? "
|
299
|
-
: "
|
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
|
-
|
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="
|
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("
|
34
|
+
const tid = toast.loading(t("login.loggingIn"))
|
32
35
|
try {
|
33
36
|
if (!email || !password) {
|
34
37
|
setIsLoading(false)
|
35
|
-
toast.error("
|
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("
|
44
|
+
toast.error(t("login.invalidCredentials"), { id: tid })
|
42
45
|
return
|
43
46
|
}
|
44
|
-
toast.success("
|
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("
|
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("
|
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("
|
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("
|
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
|
-
|
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="
|
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="
|
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 ? "
|
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
|
-
|
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
|
-
|
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
|
-
|
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}
|
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}
|
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">
|
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="
|
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 ? "
|
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
|
-
|
218
|
+
{t("login.skip")}
|
215
219
|
</button>
|
216
220
|
</div>
|
217
221
|
<div className="text-sm text-gray-600 mt-2">
|
218
|
-
|
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
|
-
|
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
|
-
|
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">
|
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
|
-
|
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">
|