@learnpack/learnpack 5.0.172 → 5.0.178
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.js +42 -2
- package/lib/creatorDist/assets/index-BvrB0WCf.js +32991 -0
- package/{src/creatorDist/assets/index-C_YTggyk.css → lib/creatorDist/assets/index-CrWESWmj.css} +43 -11
- package/lib/creatorDist/index.html +2 -2
- package/lib/utils/api.js +1 -0
- package/lib/utils/readDocuments.d.ts +0 -0
- package/lib/utils/readDocuments.js +1 -0
- package/oclif.manifest.json +1 -1
- package/package.json +3 -1
- package/src/commands/serve.ts +56 -2
- package/src/creator/src/App.tsx +51 -33
- package/src/creator/src/components/ConsumablesManager.tsx +1 -0
- package/src/creator/src/components/FileUploader.tsx +64 -52
- package/src/creator/src/components/Login.tsx +172 -82
- package/src/creator/src/components/ResumeCourseModal.tsx +38 -0
- package/src/creator/src/components/StepWizard.tsx +12 -10
- package/src/creator/src/components/TurnstileChallenge.tsx +2 -7
- package/src/creator/src/components/syllabus/ContentIndex.tsx +1 -0
- package/src/creator/src/components/syllabus/SyllabusEditor.tsx +54 -20
- package/src/creator/src/utils/constants.ts +1 -0
- package/src/creator/src/utils/lib.ts +55 -0
- package/src/creator/src/utils/rigo.ts +11 -4
- package/src/creator/src/utils/store.ts +22 -1
- package/src/creatorDist/assets/index-BvrB0WCf.js +32991 -0
- package/{lib/creatorDist/assets/index-C_YTggyk.css → src/creatorDist/assets/index-CrWESWmj.css} +43 -11
- package/src/creatorDist/index.html +2 -2
- package/src/utils/api.ts +1 -0
- package/src/utils/readDocuments.ts +0 -0
- package/lib/creatorDist/assets/index-CCvMFC6N.js +0 -83701
- package/lib/creatorDist/assets/pdf.worker-DSVOJ9H9.js +0 -56037
- package/src/creatorDist/assets/index-CCvMFC6N.js +0 -83701
- package/src/creatorDist/assets/pdf.worker-DSVOJ9H9.js +0 -56037
@@ -1,12 +1,8 @@
|
|
1
1
|
import React, { useRef, useState } from "react"
|
2
|
-
import * as pdfjsLib from "pdfjs-dist"
|
3
|
-
import mammoth from "mammoth"
|
4
2
|
import { SVGS } from "../assets/svgs"
|
5
|
-
import pdfWorker from "pdfjs-dist/build/pdf.worker?worker"
|
6
3
|
import { ContentCard } from "./ContentCard"
|
7
4
|
import useStore from "../utils/store"
|
8
|
-
|
9
|
-
pdfjsLib.GlobalWorkerOptions.workerPort = new pdfWorker()
|
5
|
+
import toast from "react-hot-toast"
|
10
6
|
|
11
7
|
const allowedTypes = [
|
12
8
|
"application/pdf",
|
@@ -21,53 +17,64 @@ export interface ParsedFile {
|
|
21
17
|
}
|
22
18
|
|
23
19
|
interface FileUploaderProps {
|
24
|
-
// onResult: (files: ParsedFile[]) => void
|
25
20
|
styledAs?: "button" | "card"
|
26
21
|
}
|
27
22
|
|
28
23
|
const FileUploader: React.FC<FileUploaderProps> = ({ styledAs = "button" }) => {
|
24
|
+
const rigoToken = useStore((state) => state.auth.rigoToken)
|
29
25
|
const inputRef = useRef<HTMLInputElement>(null)
|
30
26
|
const uploadedFiles = useStore((state) => state.uploadedFiles)
|
31
27
|
const setUploadedFiles = useStore((state) => state.setUploadedFiles)
|
32
28
|
const [isDragging, setIsDragging] = useState(false)
|
33
|
-
|
29
|
+
const [isLoading, setIsLoading] = useState(false)
|
34
30
|
|
35
|
-
const
|
31
|
+
const extractTextFromFile = async (file: File): Promise<ParsedFile> => {
|
36
32
|
const { type, name } = file
|
37
33
|
|
38
|
-
if (type === "
|
39
|
-
const
|
40
|
-
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise
|
41
|
-
let text = ""
|
42
|
-
|
43
|
-
for (let i = 0; i < pdf.numPages; i++) {
|
44
|
-
const page = await pdf.getPage(i + 1)
|
45
|
-
const content = await page.getTextContent()
|
46
|
-
text += content.items.map((item: any) => item.str).join(" ") + "\n"
|
47
|
-
}
|
48
|
-
|
34
|
+
if (type === "text/plain" || type === "text/markdown") {
|
35
|
+
const text = await file.text()
|
49
36
|
return { name, text }
|
50
37
|
}
|
51
38
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
const
|
58
|
-
|
39
|
+
const formData = new FormData()
|
40
|
+
formData.append("file", file)
|
41
|
+
|
42
|
+
const loadingToast = toast.loading(`Processing ${file.name}...`)
|
43
|
+
try {
|
44
|
+
const res = await fetch("http://localhost:3000/read-document", {
|
45
|
+
method: "POST",
|
46
|
+
headers: {
|
47
|
+
"x-rigo-token": rigoToken,
|
48
|
+
},
|
49
|
+
body: formData,
|
50
|
+
})
|
51
|
+
|
52
|
+
if (!res.ok) throw new Error(`Failed to read ${file.name}`)
|
53
|
+
const data = await res.json()
|
54
|
+
console.log("DATA FROM BACKEND", data)
|
55
|
+
toast.success(`✅ ${file.name} processed`, { id: loadingToast })
|
56
|
+
return { name, text: data.content }
|
57
|
+
} catch (err: any) {
|
58
|
+
toast.error(`❌ ${file.name} failed: ${err.message}`, {
|
59
|
+
id: loadingToast,
|
60
|
+
})
|
61
|
+
return { name, text: "" }
|
59
62
|
}
|
60
|
-
|
61
|
-
const text = await file.text()
|
62
|
-
return { name, text }
|
63
63
|
}
|
64
64
|
|
65
65
|
const parseFiles = async (files: FileList | File[]) => {
|
66
66
|
const validFiles = Array.from(files).filter((file) =>
|
67
67
|
allowedTypes.includes(file.type)
|
68
68
|
)
|
69
|
-
|
69
|
+
if (validFiles.length === 0) {
|
70
|
+
toast.error("No valid files selected")
|
71
|
+
return
|
72
|
+
}
|
73
|
+
|
74
|
+
setIsLoading(true)
|
75
|
+
const parsed = await Promise.all(validFiles.map(extractTextFromFile))
|
70
76
|
setUploadedFiles([...uploadedFiles, ...parsed])
|
77
|
+
setIsLoading(false)
|
71
78
|
}
|
72
79
|
|
73
80
|
const handleInput = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
@@ -86,16 +93,16 @@ const FileUploader: React.FC<FileUploaderProps> = ({ styledAs = "button" }) => {
|
|
86
93
|
return (
|
87
94
|
<div className="flex flex-col gap-2 w-full">
|
88
95
|
{uploadedFiles.length > 0 && styledAs === "card" && (
|
89
|
-
<div className="w-full flex flex-row gap-2 flex-wrap justify-center items-center
|
96
|
+
<div className="w-full flex flex-row gap-2 flex-wrap justify-center items-center">
|
90
97
|
{uploadedFiles.map((file, idx) => (
|
91
98
|
<div
|
92
99
|
key={idx}
|
93
|
-
className="p-3 rounded-md bg-white shadow-sm text-sm text-gray-800 text-left
|
100
|
+
className="p-3 rounded-md bg-white shadow-sm text-sm text-gray-800 text-left"
|
94
101
|
title={file.name}
|
95
102
|
>
|
96
103
|
<strong>{file.name.slice(0, 20)}...</strong>
|
97
104
|
<button
|
98
|
-
className="text-gray-600 mt-1 float-right
|
105
|
+
className="text-gray-600 mt-1 float-right cursor-pointer"
|
99
106
|
onClick={() =>
|
100
107
|
setUploadedFiles(uploadedFiles.filter((_, i) => i !== idx))
|
101
108
|
}
|
@@ -106,6 +113,7 @@ const FileUploader: React.FC<FileUploaderProps> = ({ styledAs = "button" }) => {
|
|
106
113
|
))}
|
107
114
|
</div>
|
108
115
|
)}
|
116
|
+
|
109
117
|
{styledAs === "button" && (
|
110
118
|
<div className="flex items-center justify-end gap-2 w-100">
|
111
119
|
<button
|
@@ -113,30 +121,34 @@ const FileUploader: React.FC<FileUploaderProps> = ({ styledAs = "button" }) => {
|
|
113
121
|
className="cursor-pointer blue-on-hover flex items-center justify-center w-6 h-6"
|
114
122
|
onClick={() => inputRef.current?.click()}
|
115
123
|
>
|
116
|
-
{
|
124
|
+
{isLoading ? (
|
125
|
+
<div className="loader w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
|
126
|
+
) : (
|
127
|
+
SVGS.clip
|
128
|
+
)}
|
117
129
|
</button>
|
118
130
|
</div>
|
119
131
|
)}
|
120
132
|
|
121
133
|
{styledAs === "card" && (
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
134
|
+
<ContentCard
|
135
|
+
description={
|
136
|
+
isDragging
|
137
|
+
? "Drop it here"
|
138
|
+
: isLoading
|
139
|
+
? "Processing..."
|
140
|
+
: "Upload a PDF or DOCX file or drag it here"
|
141
|
+
}
|
142
|
+
icon={isDragging ? SVGS.clip : SVGS.pdf}
|
143
|
+
onClick={() => inputRef.current?.click()}
|
144
|
+
onDragOver={(e) => {
|
145
|
+
e.preventDefault()
|
146
|
+
setIsDragging(true)
|
147
|
+
}}
|
148
|
+
onDragLeave={() => setIsDragging(false)}
|
149
|
+
onDrop={handleDrop}
|
150
|
+
className={isDragging ? "border-blue-600 bg-blue-50" : ""}
|
151
|
+
/>
|
140
152
|
)}
|
141
153
|
|
142
154
|
<input
|
@@ -4,61 +4,88 @@ import { SVGS } from "../assets/svgs"
|
|
4
4
|
import toast from "react-hot-toast"
|
5
5
|
import useStore from "../utils/store"
|
6
6
|
import { useShallow } from "zustand/react/shallow"
|
7
|
-
import { login4Geeks } from "../utils/lib"
|
7
|
+
import { login4Geeks, registerUserWithFormData } from "../utils/lib"
|
8
8
|
|
9
9
|
export default function Login({ onFinish }: { onFinish: () => void }) {
|
10
|
+
// Login states
|
10
11
|
const [email, setEmail] = useState("")
|
11
12
|
const [password, setPassword] = useState("")
|
12
13
|
const [isLoading, setIsLoading] = useState(false)
|
13
14
|
const [showForm, setShowForm] = useState(false)
|
14
|
-
|
15
|
-
|
15
|
+
// Signup states
|
16
|
+
const [showSignup, setShowSignup] = useState(false)
|
17
|
+
const [signupData, setSignupData] = useState({
|
18
|
+
firstName: "",
|
19
|
+
lastName: "",
|
20
|
+
email: "",
|
21
|
+
})
|
22
|
+
// const planToRedirect = useStore((state) => state.planToRedirect)
|
16
23
|
const { setAuth } = useStore(
|
17
24
|
useShallow((state) => ({ setAuth: state.setAuth }))
|
18
25
|
)
|
19
26
|
|
20
|
-
|
27
|
+
// Login handler
|
28
|
+
const login = async (e: React.FormEvent<HTMLFormElement>) => {
|
21
29
|
e.preventDefault()
|
22
30
|
setIsLoading(true)
|
23
31
|
const tid = toast.loading("Logging in…")
|
24
|
-
|
25
32
|
if (!email || !password) {
|
26
33
|
setIsLoading(false)
|
27
34
|
toast.error("Please fill all fields", { id: tid })
|
28
35
|
return
|
29
36
|
}
|
30
|
-
|
31
37
|
const resp = await login4Geeks({ email, password })
|
32
38
|
if (!resp) {
|
33
39
|
setIsLoading(false)
|
34
40
|
toast.error("Invalid credentials", { id: tid })
|
35
41
|
return
|
36
42
|
}
|
37
|
-
|
38
43
|
toast.success("Logged in successfully", { id: tid })
|
39
44
|
setAuth({
|
40
45
|
bcToken: resp.token,
|
41
46
|
userId: resp.user.id,
|
42
47
|
rigoToken: resp.rigobot.key,
|
43
48
|
user: resp.user,
|
49
|
+
publicToken: "",
|
44
50
|
})
|
45
51
|
setIsLoading(false)
|
46
52
|
onFinish()
|
47
53
|
}
|
48
54
|
|
55
|
+
// Signup handler
|
56
|
+
const handleSignup = async (e: React.FormEvent<HTMLFormElement>) => {
|
57
|
+
e.preventDefault()
|
58
|
+
setIsLoading(true)
|
59
|
+
const tid = toast.loading("Creating account…")
|
60
|
+
const { firstName, lastName, email } = signupData
|
61
|
+
if (!firstName || !lastName || !email) {
|
62
|
+
setIsLoading(false)
|
63
|
+
toast.error("Please fill all fields", { id: tid })
|
64
|
+
return
|
65
|
+
}
|
66
|
+
try {
|
67
|
+
await registerUserWithFormData(firstName, lastName, email)
|
68
|
+
toast.success("Account created! Check your email.", { id: tid })
|
69
|
+
setShowSignup(false)
|
70
|
+
setShowForm(true)
|
71
|
+
setEmail(email)
|
72
|
+
} catch (err) {
|
73
|
+
toast.error("Registration failed. Try again.", { id: tid })
|
74
|
+
} finally {
|
75
|
+
setIsLoading(false)
|
76
|
+
}
|
77
|
+
}
|
78
|
+
|
49
79
|
function stringToBase64(str: string) {
|
50
80
|
return btoa(unescape(encodeURIComponent(str)))
|
51
81
|
}
|
52
|
-
|
53
82
|
function getCurrentUrlWithQueryParams() {
|
54
83
|
return window.location.href
|
55
84
|
}
|
56
|
-
|
57
85
|
const redirectGithub = () => {
|
58
86
|
const url = stringToBase64(getCurrentUrlWithQueryParams())
|
59
87
|
window.location.href = `${BREATHECODE_HOST}/v1/auth/github/?url=${url}`
|
60
88
|
}
|
61
|
-
|
62
89
|
const redirectGoogle = () => {
|
63
90
|
const url = stringToBase64(getCurrentUrlWithQueryParams())
|
64
91
|
window.location.href = `${BREATHECODE_HOST}/v1/auth/google?url=${url}`
|
@@ -73,89 +100,152 @@ export default function Login({ onFinish }: { onFinish: () => void }) {
|
|
73
100
|
className="bg-white p-8 rounded-xl shadow-md max-w-sm w-full"
|
74
101
|
onClick={(e) => e.stopPropagation()}
|
75
102
|
>
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
placeholder="Password"
|
110
|
-
className="w-full border border-gray-300 px-3 py-2 rounded-md"
|
111
|
-
onChange={(e) => setPassword(e.target.value)}
|
112
|
-
/>
|
113
|
-
<div className="flex gap-2 mt-4">
|
103
|
+
{showSignup ? (
|
104
|
+
<>
|
105
|
+
<h2 className="mb-4 text-xl font-semibold text-center">
|
106
|
+
Create your account
|
107
|
+
</h2>
|
108
|
+
<form className="space-y-3" onSubmit={handleSignup}>
|
109
|
+
<input
|
110
|
+
type="text"
|
111
|
+
placeholder="First name"
|
112
|
+
className="w-full border border-gray-300 px-3 py-2 rounded-md"
|
113
|
+
value={signupData.firstName}
|
114
|
+
onChange={(e) =>
|
115
|
+
setSignupData({ ...signupData, firstName: e.target.value })
|
116
|
+
}
|
117
|
+
/>
|
118
|
+
<input
|
119
|
+
type="text"
|
120
|
+
placeholder="Last name"
|
121
|
+
className="w-full border border-gray-300 px-3 py-2 rounded-md"
|
122
|
+
value={signupData.lastName}
|
123
|
+
onChange={(e) =>
|
124
|
+
setSignupData({ ...signupData, lastName: e.target.value })
|
125
|
+
}
|
126
|
+
/>
|
127
|
+
<input
|
128
|
+
type="email"
|
129
|
+
placeholder="Email"
|
130
|
+
className="w-full border border-gray-300 px-3 py-2 rounded-md"
|
131
|
+
value={signupData.email}
|
132
|
+
onChange={(e) =>
|
133
|
+
setSignupData({ ...signupData, email: e.target.value })
|
134
|
+
}
|
135
|
+
/>
|
114
136
|
<button
|
115
137
|
type="submit"
|
116
|
-
className="
|
138
|
+
className="w-full bg-blue-600 text-white py-2 rounded-md font-semibold cursor-pointer"
|
139
|
+
disabled={isLoading}
|
117
140
|
>
|
118
|
-
{isLoading ? "
|
141
|
+
{isLoading ? "Creating..." : "Sign up"}
|
119
142
|
</button>
|
143
|
+
</form>
|
144
|
+
<div className="mt-4 text-sm text-center">
|
145
|
+
Already have an account?{" "}
|
120
146
|
<button
|
121
|
-
|
122
|
-
onClick={() =>
|
123
|
-
|
147
|
+
className="text-blue-600 font-medium"
|
148
|
+
onClick={() => {
|
149
|
+
setShowSignup(false)
|
150
|
+
setShowForm(true)
|
151
|
+
}}
|
124
152
|
>
|
125
|
-
|
153
|
+
Log in here
|
126
154
|
</button>
|
127
155
|
</div>
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
156
|
+
</>
|
157
|
+
) : (
|
158
|
+
<>
|
159
|
+
<p className="mb-4 text-center text-gray-700">
|
160
|
+
You need to have a 4Geeks account with a creator plan to create a
|
161
|
+
tutorial.
|
162
|
+
</p>
|
163
|
+
<button
|
164
|
+
onClick={redirectGithub}
|
165
|
+
className="w-full border border-gray-300 py-2 rounded-md font-semibold flex items-center justify-center gap-2 mb-4 cursor-pointer"
|
166
|
+
>
|
167
|
+
{SVGS.github} LOGIN WITH GITHUB
|
168
|
+
</button>
|
169
|
+
<button
|
170
|
+
onClick={redirectGoogle}
|
171
|
+
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
|
+
>
|
173
|
+
{SVGS.google} LOGIN WITH GOOGLE
|
174
|
+
</button>
|
175
|
+
<div className="flex items-center mb-4">
|
176
|
+
<hr className="flex-grow border-gray-300" />
|
177
|
+
<span className="mx-2 text-gray-400 text-sm">or</span>
|
178
|
+
<hr className="flex-grow border-gray-300" />
|
179
|
+
</div>
|
180
|
+
{showForm ? (
|
181
|
+
<form className="space-y-3" onSubmit={login}>
|
182
|
+
<input
|
183
|
+
type="email"
|
184
|
+
placeholder="Email"
|
185
|
+
className="w-full border border-gray-300 px-3 py-2 rounded-md"
|
186
|
+
value={email}
|
187
|
+
onChange={(e) => setEmail(e.target.value)}
|
188
|
+
/>
|
189
|
+
<input
|
190
|
+
type="password"
|
191
|
+
placeholder="Password"
|
192
|
+
className="w-full border border-gray-300 px-3 py-2 rounded-md"
|
193
|
+
value={password}
|
194
|
+
onChange={(e) => setPassword(e.target.value)}
|
195
|
+
/>
|
196
|
+
<div className="flex gap-2 mt-4">
|
197
|
+
<button
|
198
|
+
type="submit"
|
199
|
+
className="flex-1 bg-blue-500 text-white py-2 rounded-md font-semibold cursor-pointer"
|
200
|
+
>
|
201
|
+
{isLoading ? "Logging in..." : "Log in"}
|
202
|
+
</button>
|
203
|
+
<button
|
204
|
+
type="button"
|
205
|
+
onClick={() => setShowForm(false)}
|
206
|
+
className="flex-1 border border-blue-500 text-blue-600 py-2 rounded-md font-semibold cursor-pointer"
|
207
|
+
>
|
208
|
+
Skip
|
209
|
+
</button>
|
210
|
+
</div>
|
211
|
+
<div className="text-sm text-gray-600 mt-2">
|
212
|
+
Forgot your password?{" "}
|
213
|
+
<a
|
214
|
+
href={`${BREATHECODE_HOST}/v1/auth/password/reset?url=${getCurrentUrlWithQueryParams()}`}
|
215
|
+
className="text-blue-600 font-medium"
|
216
|
+
>
|
217
|
+
Recover it here.
|
218
|
+
</a>
|
219
|
+
</div>
|
220
|
+
</form>
|
221
|
+
) : (
|
222
|
+
<button
|
223
|
+
onClick={() => setShowForm(true)}
|
224
|
+
className="w-full bg-blue-600 text-white py-2 rounded-md font-semibold cursor-pointer"
|
133
225
|
>
|
134
|
-
|
135
|
-
</
|
226
|
+
Login with Email
|
227
|
+
</button>
|
228
|
+
)}
|
229
|
+
<div className="flex justify-between items-center mt-4">
|
230
|
+
<div className="bg-blue-50 text-sm p-2 rounded text-left">
|
231
|
+
<p className="text-gray-700 m-0">You don't have an account?</p>
|
232
|
+
{/* <button
|
233
|
+
className="text-blue-600 font-medium cursor-pointer"
|
234
|
+
onClick={() => setShowSignup(true)}
|
235
|
+
>
|
236
|
+
Register here.
|
237
|
+
</button> */}
|
238
|
+
<a
|
239
|
+
href={`https://www.learnpack.co/register`}
|
240
|
+
target="_blank"
|
241
|
+
className="text-blue-600 font-medium"
|
242
|
+
>
|
243
|
+
Register here.
|
244
|
+
</a>
|
245
|
+
</div>
|
136
246
|
</div>
|
137
|
-
|
138
|
-
) : (
|
139
|
-
<button
|
140
|
-
onClick={() => setShowForm(true)}
|
141
|
-
className="w-full bg-blue-600 text-white py-2 rounded-md font-semibold cursor-pointer"
|
142
|
-
>
|
143
|
-
Login with Email
|
144
|
-
</button>
|
247
|
+
</>
|
145
248
|
)}
|
146
|
-
|
147
|
-
<div className="flex justify-between items-center mt-4">
|
148
|
-
<div className="bg-blue-50 text-sm p-2 rounded text-left">
|
149
|
-
<p className="text-gray-700 m-0">You don't have an account?</p>
|
150
|
-
<a
|
151
|
-
href={`https://4geeks.com/checkout?plan=${planToRedirect}`}
|
152
|
-
target="_blank"
|
153
|
-
className="text-blue-600 font-medium"
|
154
|
-
>
|
155
|
-
Register here.
|
156
|
-
</a>
|
157
|
-
</div>
|
158
|
-
</div>
|
159
249
|
</div>
|
160
250
|
</div>
|
161
251
|
)
|
@@ -0,0 +1,38 @@
|
|
1
|
+
interface ResumeCourseModalProps {
|
2
|
+
onContinue: () => void
|
3
|
+
onStartOver: () => void
|
4
|
+
}
|
5
|
+
|
6
|
+
export default function ResumeCourseModal({
|
7
|
+
onContinue,
|
8
|
+
onStartOver,
|
9
|
+
}: ResumeCourseModalProps) {
|
10
|
+
return (
|
11
|
+
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
|
12
|
+
<div className="bg-white shadow-xl rounded-lg p-8 max-w-md w-full animate-fade-in">
|
13
|
+
<h2 className="text-xl font-bold text-gray-800 mb-3 text-center">
|
14
|
+
You have an unfinished course
|
15
|
+
</h2>
|
16
|
+
<p className="text-gray-600 mb-6 text-center">
|
17
|
+
You didn't finish creating your previous course.
|
18
|
+
<br />
|
19
|
+
What would you like to do?
|
20
|
+
</p>
|
21
|
+
<div className="flex flex-col gap-4">
|
22
|
+
<button
|
23
|
+
onClick={onContinue}
|
24
|
+
className="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 rounded-md font-semibold transition cursor-pointer"
|
25
|
+
>
|
26
|
+
Continue previous course
|
27
|
+
</button>
|
28
|
+
<button
|
29
|
+
onClick={onStartOver}
|
30
|
+
className="w-full border border-blue-600 text-blue-700 py-2 rounded-md font-semibold hover:bg-blue-50 transition cursor-pointer"
|
31
|
+
>
|
32
|
+
Erase previous answers and start a new course
|
33
|
+
</button>
|
34
|
+
</div>
|
35
|
+
</div>
|
36
|
+
</div>
|
37
|
+
)
|
38
|
+
}
|
@@ -17,6 +17,7 @@ type Props = {
|
|
17
17
|
formState: any
|
18
18
|
setFormState: (formState: any) => void
|
19
19
|
onFinish: () => void
|
20
|
+
hideLastButton?: boolean
|
20
21
|
}
|
21
22
|
|
22
23
|
const StepWizard: React.FC<Props> = ({
|
@@ -24,6 +25,7 @@ const StepWizard: React.FC<Props> = ({
|
|
24
25
|
formState,
|
25
26
|
setFormState,
|
26
27
|
onFinish,
|
28
|
+
hideLastButton = false,
|
27
29
|
}) => {
|
28
30
|
const currentStep = formState.currentStep
|
29
31
|
const index = steps.findIndex((step) => step.slug === currentStep)
|
@@ -48,8 +50,6 @@ const StepWizard: React.FC<Props> = ({
|
|
48
50
|
setFormState({ ...formState, currentStep: steps[index - 1].slug })
|
49
51
|
}
|
50
52
|
|
51
|
-
console.log(index, totalSteps, steps)
|
52
|
-
|
53
53
|
return (
|
54
54
|
<div className="min-h-screen flex flex-col items-center justify-center text-center px-4">
|
55
55
|
<div className=" rounded-xl p-8 w-full max-w-xl">
|
@@ -99,14 +99,16 @@ const StepWizard: React.FC<Props> = ({
|
|
99
99
|
>
|
100
100
|
⬅ Back
|
101
101
|
</button>
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
102
|
+
{!(hideLastButton && index === totalSteps - 1) && (
|
103
|
+
<button
|
104
|
+
onClick={index === totalSteps - 1 ? onFinish : goNext}
|
105
|
+
className={`text-sm text-blue-600 hover:text-blue-800 cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed p-2 rounded-md ${
|
106
|
+
index === totalSteps - 1 ? "bg-blue-600 text-white" : ""
|
107
|
+
}`}
|
108
|
+
>
|
109
|
+
{index === totalSteps - 1 ? "Finish 🚀" : "Next ➡"}
|
110
|
+
</button>
|
111
|
+
)}
|
110
112
|
</div>
|
111
113
|
</div>
|
112
114
|
</div>
|
@@ -1,6 +1,6 @@
|
|
1
1
|
// components/TurnstileChallenge.tsx
|
2
2
|
import { useEffect, useRef, useState } from "react"
|
3
|
-
import toast from "react-hot-toast"
|
3
|
+
// import toast from "react-hot-toast"
|
4
4
|
|
5
5
|
interface TurnstileChallengeProps {
|
6
6
|
siteKey: string
|
@@ -34,8 +34,6 @@ const TurnstileChallenge = ({
|
|
34
34
|
const id = window.turnstile.render(`#${containerId}`, {
|
35
35
|
sitekey: siteKey,
|
36
36
|
callback: (token: string) => {
|
37
|
-
console.log("token to send", token)
|
38
|
-
|
39
37
|
onSuccess(token)
|
40
38
|
},
|
41
39
|
"error-callback": () => {
|
@@ -43,7 +41,7 @@ const TurnstileChallenge = ({
|
|
43
41
|
},
|
44
42
|
"refresh-callback": () => {
|
45
43
|
console.log("refresh callback CALLEd")
|
46
|
-
toast.error("Refresh callback called")
|
44
|
+
// toast.error("Refresh callback called")
|
47
45
|
},
|
48
46
|
})
|
49
47
|
|
@@ -61,11 +59,8 @@ const TurnstileChallenge = ({
|
|
61
59
|
return () => clearInterval(interval)
|
62
60
|
}, [siteKey, autoStart, onSuccess, onError])
|
63
61
|
|
64
|
-
|
65
62
|
return (
|
66
|
-
|
67
63
|
<div className="">
|
68
|
-
|
69
64
|
<div id={containerId} data-refresh-timeout="auto" />
|
70
65
|
{!ready && <p className="">...</p>}
|
71
66
|
{/* <button onClick={reset}>RESET</button> */}
|