@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.
- package/README.md +13 -13
- package/lib/commands/serve.d.ts +5 -28
- package/lib/commands/serve.js +45 -14
- package/lib/creatorDist/assets/index-hTGEtFj4.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 +58 -49
- package/src/creator/package-lock.json +97 -1
- package/src/creator/package.json +3 -0
- package/src/creator/src/App.tsx +48 -22
- 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 +4 -3
- package/src/creator/src/components/syllabus/SyllabusEditor.tsx +18 -2
- 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 +2 -2
- package/src/creator/src/utils/lib.ts +7 -0
- package/src/creator/src/utils/store.ts +8 -5
- package/src/creatorDist/assets/index-hTGEtFj4.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-DszesaEz.js +0 -35438
- package/src/creatorDist/assets/index-DszesaEz.js +0 -35438
package/src/creator/src/App.tsx
CHANGED
@@ -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: "
|
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="
|
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: "
|
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="
|
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="
|
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="
|
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: "
|
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: "
|
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("
|
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: "
|
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="
|
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="
|
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: "
|
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="
|
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
|
-
? "
|
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">
|
@@ -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("
|
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
|
-
"
|
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
|
-
⬅
|
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
|
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="
|
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
|
-
|
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="
|
47
|
+
description={t("uploader.text.description")}
|
45
48
|
icon={SVGS.contentTable}
|
46
49
|
onClick={() => handleSelectOption("text")}
|
47
50
|
/>
|
48
51
|
<ContentCard
|
49
|
-
description="
|
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="
|
58
|
+
description={t("uploader.youtube.description")}
|
56
59
|
icon={SVGS.youtube}
|
57
60
|
onClick={() => handleSelectOption("youtube")}
|
58
61
|
/>
|