@learnpack/learnpack 5.0.316 → 5.0.317

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 (33) hide show
  1. package/lib/commands/serve.js +77 -26
  2. package/lib/creatorDist/assets/{index-D4SYZg0r.css → index-CjddKHB_.css} +12 -0
  3. package/lib/creatorDist/assets/{index-DxgQXudf.js → index-DRIj_L1d.js} +38341 -34249
  4. package/lib/creatorDist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/src/commands/serve.ts +3267 -3204
  7. package/src/creator/components.json +18 -0
  8. package/src/creator/package-lock.json +470 -1
  9. package/src/creator/package.json +3 -0
  10. package/src/creator/src/assets/svgs.tsx +0 -1
  11. package/src/creator/src/components/FileCard.tsx +15 -3
  12. package/src/creator/src/components/FileUploader.tsx +41 -23
  13. package/src/creator/src/components/LessonItem.tsx +80 -49
  14. package/src/creator/src/components/LinkUploader.tsx +55 -8
  15. package/src/creator/src/components/Source.tsx +15 -6
  16. package/src/creator/src/components/syllabus/ContentIndex.tsx +19 -7
  17. package/src/creator/src/components/syllabus/Sidebar.tsx +56 -48
  18. package/src/creator/src/components/ui/tooltip.tsx +31 -0
  19. package/src/creator/src/lib/utils.ts +6 -0
  20. package/src/creator/src/locales/en.json +24 -1
  21. package/src/creator/src/locales/es.json +24 -1
  22. package/src/creator/src/main.tsx +11 -7
  23. package/src/creator/src/utils/rigo.ts +85 -85
  24. package/src/creator/tsconfig.app.json +6 -0
  25. package/src/creator/vite.config.ts +10 -4
  26. package/src/creatorDist/assets/{index-D4SYZg0r.css → index-CjddKHB_.css} +12 -0
  27. package/src/creatorDist/assets/{index-DxgQXudf.js → index-DRIj_L1d.js} +38341 -34249
  28. package/src/creatorDist/index.html +2 -2
  29. package/src/ui/_app/app.css +1 -1
  30. package/src/ui/_app/app.js +529 -529
  31. package/src/ui/_app/learnpack.svg +7 -7
  32. package/src/ui/_app/sw.js +59 -59
  33. package/src/ui/app.tar.gz +0 -0
@@ -7,6 +7,11 @@ 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
+ import {
11
+ Tooltip,
12
+ TooltipContent,
13
+ TooltipTrigger,
14
+ } from "@/components/ui/tooltip"
10
15
 
11
16
  const socketClient = new CreatorSocket("")
12
17
 
@@ -80,17 +85,18 @@ const UploadedFileCard = ({ idx, file }: { idx: number; file: ParsedFile }) => {
80
85
  }, [])
81
86
 
82
87
  return (
83
- <div
84
- className={
85
- "p-3 rounded-md shadow-sm text-sm text-gray-800 text-left flex items-center gap-2" +
86
- (file.status === "PROCESSING"
87
- ? " bg-gray-100"
88
- : file.status === "ERROR"
89
- ? " bg-red-100"
90
- : " bg-white")
91
- }
92
- title={file.name}
93
- >
88
+ <Tooltip>
89
+ <TooltipTrigger asChild>
90
+ <div
91
+ className={
92
+ "p-3 rounded-md shadow-sm text-sm text-gray-800 text-left flex items-center gap-2" +
93
+ (file.status === "PROCESSING"
94
+ ? " bg-gray-100"
95
+ : file.status === "ERROR"
96
+ ? " bg-red-100"
97
+ : " bg-white")
98
+ }
99
+ >
94
100
  {file.status === "PROCESSING" && (
95
101
  <div className="w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
96
102
  )}
@@ -113,7 +119,12 @@ const UploadedFileCard = ({ idx, file }: { idx: number; file: ParsedFile }) => {
113
119
  >
114
120
  {SVGS.trash}
115
121
  </button>
116
- </div>
122
+ </div>
123
+ </TooltipTrigger>
124
+ <TooltipContent>
125
+ <p>{file.name}</p>
126
+ </TooltipContent>
127
+ </Tooltip>
117
128
  )
118
129
  }
119
130
 
@@ -256,17 +267,24 @@ const FileUploader: React.FC<FileUploaderProps> = ({
256
267
 
257
268
  {styledAs === "button" && (
258
269
  <div className="flex items-center justify-end gap-2 w-100">
259
- <button
260
- type="button"
261
- className="cursor-pointer blue-on-hover flex items-center justify-center w-6 h-6"
262
- onClick={() => inputRef.current?.click()}
263
- >
264
- {isLoading ? (
265
- <div className="loader w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
266
- ) : (
267
- SVGS.clip
268
- )}
269
- </button>
270
+ <Tooltip>
271
+ <TooltipTrigger asChild>
272
+ <button
273
+ type="button"
274
+ className="cursor-pointer blue-on-hover flex items-center justify-center w-6 h-6"
275
+ onClick={() => inputRef.current?.click()}
276
+ >
277
+ {isLoading ? (
278
+ <div className="loader w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
279
+ ) : (
280
+ SVGS.clip
281
+ )}
282
+ </button>
283
+ </TooltipTrigger>
284
+ <TooltipContent>
285
+ <p>{t("tooltips.attachFiles")}</p>
286
+ </TooltipContent>
287
+ </Tooltip>
270
288
  </div>
271
289
  )}
272
290
 
@@ -6,6 +6,12 @@ import { useRef } from "react"
6
6
  import { SVGS } from "../assets/svgs"
7
7
  import useStore from "../utils/store"
8
8
  import { Icon } from "./Icon"
9
+ import {
10
+ Tooltip,
11
+ TooltipContent,
12
+ TooltipTrigger,
13
+ } from "@/components/ui/tooltip"
14
+ import { useTranslation } from "react-i18next"
9
15
 
10
16
  export interface Lesson {
11
17
  id: string
@@ -56,6 +62,7 @@ export const LessonItem: React.FC<LessonItemProps> = ({
56
62
  const mode = useStore((state) => state.mode)
57
63
  const inputRef = useRef<HTMLInputElement>(null)
58
64
  const [isEditing, setIsEditing] = useState(false)
65
+ const { t } = useTranslation()
59
66
 
60
67
  return (
61
68
  <div
@@ -96,55 +103,79 @@ export const LessonItem: React.FC<LessonItemProps> = ({
96
103
 
97
104
  {mode === "teacher" && (
98
105
  <>
99
- <button
100
- onClick={() => onToggleLock?.(lesson.uid)}
101
- className={`cursor-pointer ${
102
- lesson.locked
103
- ? "text-yellow-600 hover:text-yellow-700"
104
- : "text-gray-400 hover:text-yellow-600"
105
- }`}
106
- title={lesson.locked ? "Unlock lesson" : "Lock lesson"}
107
- >
108
- <Icon
109
- name={lesson.locked ? "Lock" : "Unlock"}
110
- size={16}
111
- />
112
- </button>
113
- <button
114
- onClick={() => {
115
- if (!lesson.locked) {
116
- setIsEditing(!isEditing)
117
- if (inputRef.current) {
118
- onChange(lesson.uid, inputRef.current.value)
119
- }
120
- }
121
- }}
122
- className={`${
123
- lesson.locked
124
- ? "text-gray-300 cursor-not-allowed"
125
- : "text-gray-500 hover:text-blue-500 cursor-pointer"
126
- }`}
127
- disabled={lesson.locked}
128
- title={lesson.locked ? "Cannot edit locked lesson" : "Edit lesson"}
129
- >
130
- <Icon name="Edit" size={16} />
131
- </button>
132
- <button
133
- onClick={() => {
134
- if (!lesson.locked) {
135
- onRemove()
136
- }
137
- }}
138
- className={`${
139
- lesson.locked
140
- ? "text-gray-300 cursor-not-allowed"
141
- : "text-red-500 hover:text-red-700 cursor-pointer"
142
- }`}
143
- disabled={lesson.locked}
144
- title={lesson.locked ? "Cannot delete locked lesson" : "Delete lesson"}
145
- >
146
- {SVGS.trash}
147
- </button>
106
+ <Tooltip>
107
+ <TooltipTrigger asChild>
108
+ <span className="inline-flex">
109
+ <button
110
+ onClick={() => onToggleLock?.(lesson.uid)}
111
+ className={`cursor-pointer ${
112
+ lesson.locked
113
+ ? "text-yellow-600 hover:text-yellow-700"
114
+ : "text-gray-400 hover:text-yellow-600"
115
+ }`}
116
+ >
117
+ <Icon
118
+ name={lesson.locked ? "Lock" : "Unlock"}
119
+ size={16}
120
+ />
121
+ </button>
122
+ </span>
123
+ </TooltipTrigger>
124
+ <TooltipContent>
125
+ <p>{lesson.locked ? t("tooltips.unlockLesson") : t("tooltips.lockLesson")}</p>
126
+ </TooltipContent>
127
+ </Tooltip>
128
+ <Tooltip>
129
+ <TooltipTrigger asChild>
130
+ <span className="inline-flex">
131
+ <button
132
+ onClick={() => {
133
+ if (!lesson.locked) {
134
+ setIsEditing(!isEditing)
135
+ if (inputRef.current) {
136
+ onChange(lesson.uid, inputRef.current.value)
137
+ }
138
+ }
139
+ }}
140
+ className={`${
141
+ lesson.locked
142
+ ? "text-gray-300 cursor-not-allowed"
143
+ : "text-gray-500 hover:text-blue-500 cursor-pointer"
144
+ }`}
145
+ disabled={lesson.locked}
146
+ >
147
+ <Icon name="Edit" size={16} />
148
+ </button>
149
+ </span>
150
+ </TooltipTrigger>
151
+ <TooltipContent>
152
+ <p>{lesson.locked ? t("tooltips.cannotEditLocked") : t("tooltips.editLesson")}</p>
153
+ </TooltipContent>
154
+ </Tooltip>
155
+ <Tooltip>
156
+ <TooltipTrigger asChild>
157
+ <span className="inline-flex">
158
+ <button
159
+ onClick={() => {
160
+ if (!lesson.locked) {
161
+ onRemove()
162
+ }
163
+ }}
164
+ className={`${
165
+ lesson.locked
166
+ ? "text-gray-300 cursor-not-allowed"
167
+ : "text-red-500 hover:text-red-700 cursor-pointer"
168
+ }`}
169
+ disabled={lesson.locked}
170
+ >
171
+ {SVGS.trash}
172
+ </button>
173
+ </span>
174
+ </TooltipTrigger>
175
+ <TooltipContent>
176
+ <p>{lesson.locked ? t("tooltips.cannotDeleteLocked") : t("tooltips.deleteLesson")}</p>
177
+ </TooltipContent>
178
+ </Tooltip>
148
179
  </>
149
180
  )}
150
181
  </div>
@@ -24,24 +24,59 @@ const LinkUploader: React.FC<LinkUploaderProps> = ({ onResult }) => {
24
24
  const [url, setUrl] = useState("")
25
25
  const [loading, setLoading] = useState(false)
26
26
  const [error, setError] = useState<string | null>(null)
27
+ const [show404Help, setShow404Help] = useState(false)
27
28
 
28
29
  const handleAdd = async () => {
29
30
  const raw = url.trim()
30
- if (!raw) return setError("Please enter a URL.")
31
+ if (!raw) {
32
+ return setError(t("uploader.youtube.enterUrl"))
33
+ }
34
+
35
+ // Validate YouTube URL format
36
+ const ytRegex =
37
+ /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)[A-Za-z0-9_-]{11}/
38
+ if (!ytRegex.test(raw)) {
39
+ return setError(t("uploader.youtube.invalidUrl"))
40
+ }
41
+
31
42
  setError(null)
32
43
  setLoading(true)
33
44
 
34
45
  try {
35
46
  const key = toBase64Url(raw)
36
- const resp = await fetch(`/actions/fetch/${key}`)
37
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
47
+ const endpoint = `/actions/fetch/${key}`
48
+ const resp = await fetch(endpoint)
49
+
50
+ if (!resp.ok) {
51
+ // Handle specific error cases
52
+ if (resp.status === 404) {
53
+ setShow404Help(true)
54
+ throw new Error(t("uploader.youtube.notAvailable"))
55
+ }
56
+ if (resp.status === 429) {
57
+ const retryData = await resp.json()
58
+ throw new Error(
59
+ retryData.error || t("uploader.youtube.tooManyRequests")
60
+ )
61
+ }
62
+ if (resp.status === 500) {
63
+ const errorData = await resp.json()
64
+ throw new Error(
65
+ errorData.error || t("uploader.youtube.serviceUnavailable")
66
+ )
67
+ }
68
+ throw new Error(t("uploader.youtube.fetchError"))
69
+ }
70
+
38
71
  const data = (await resp.json()) as ParsedLink
39
72
 
40
73
  onResult(data) // emit the parsed link immediately
41
74
  setUrl("") // clear input
42
- } catch (err: any) {
75
+ setShow404Help(false)
76
+ } catch (err: unknown) {
43
77
  console.error(err)
44
- setError(err.message || "Failed to fetch link.")
78
+ const message = err instanceof Error ? err.message : t("uploader.youtube.fetchError")
79
+ setError(message)
45
80
  } finally {
46
81
  setLoading(false)
47
82
  }
@@ -57,6 +92,8 @@ const LinkUploader: React.FC<LinkUploaderProps> = ({ onResult }) => {
57
92
  onKeyDown={(e) => e.key === "Enter" && handleAdd()}
58
93
  disabled={loading}
59
94
  placeholder={t("uploader.youtube.placeholder")}
95
+ pattern="(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)[A-Za-z0-9_-]{11}"
96
+ title="Please enter a valid YouTube URL (youtube.com/watch?v=... or youtu.be/...)"
60
97
  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"
61
98
  />
62
99
  <button
@@ -96,9 +133,19 @@ const LinkUploader: React.FC<LinkUploaderProps> = ({ onResult }) => {
96
133
  </div>
97
134
 
98
135
  {error && (
99
- <p className="mt-3 text-sm text-red-600 bg-red-50 p-2 rounded">
100
- {error}
101
- </p>
136
+ <div className="mt-3 text-sm bg-red-50 p-2 rounded">
137
+ <p className="text-red-600">{error}</p>
138
+ {show404Help && (
139
+ <div className="mt-2 text-gray-700">
140
+ <p>{t("uploader.youtube.notAvailableReasons.title")}</p>
141
+ <ul className="list-disc pl-5 mt-1">
142
+ <li>{t("uploader.youtube.notAvailableReasons.captions")}</li>
143
+ <li>{t("uploader.youtube.notAvailableReasons.privacy")}</li>
144
+ <li>{t("uploader.youtube.notAvailableReasons.region")}</li>
145
+ </ul>
146
+ </div>
147
+ )}
148
+ </div>
102
149
  )}
103
150
  </div>
104
151
  )
@@ -2,6 +2,11 @@ import React from "react"
2
2
  import { ParsedLink } from "./LinkUploader"
3
3
  // import useStore from "../utils/store"
4
4
  import { SVGS } from "../assets/svgs"
5
+ import {
6
+ Tooltip,
7
+ TooltipContent,
8
+ TooltipTrigger,
9
+ } from "@/components/ui/tooltip"
5
10
 
6
11
  interface SourceProps {
7
12
  source: ParsedLink
@@ -50,12 +55,16 @@ const Source: React.FC<SourceProps> = ({ source }) => {
50
55
  />
51
56
  )}
52
57
  {title && (
53
- <span
54
- className="text-sm font-semibold text-left text-gray-800 "
55
- title={title}
56
- >
57
- {title}
58
- </span>
58
+ <Tooltip>
59
+ <TooltipTrigger asChild>
60
+ <span className="text-sm font-semibold text-left text-gray-800 ">
61
+ {title}
62
+ </span>
63
+ </TooltipTrigger>
64
+ <TooltipContent>
65
+ <p>{title}</p>
66
+ </TooltipContent>
67
+ </Tooltip>
59
68
  )}
60
69
  </div>
61
70
  </div>
@@ -9,7 +9,11 @@ import { motion, AnimatePresence } from "framer-motion"
9
9
  import toast from "react-hot-toast"
10
10
  import { randomUUID } from "../../utils/creatorUtils"
11
11
  import { useTranslation } from "react-i18next"
12
- import { t } from "i18next"
12
+ import {
13
+ Tooltip,
14
+ TooltipContent,
15
+ TooltipTrigger,
16
+ } from "@/components/ui/tooltip"
13
17
 
14
18
  const ContentIndexHeader = ({
15
19
  messages,
@@ -163,6 +167,7 @@ export const ContentIndex = ({
163
167
  const mode = useStore((state) => state.mode)
164
168
  const containerRef = useRef<HTMLDivElement>(null)
165
169
  const [showScrollHint, setShowScrollHint] = useState(false)
170
+ const { t } = useTranslation()
166
171
 
167
172
  const syllabus = history[history.length - 1]
168
173
 
@@ -282,12 +287,19 @@ export const ContentIndex = ({
282
287
  {mode === "teacher" && (
283
288
  <div className="relative h-6">
284
289
  <div className="absolute left-1/2 -translate-x-1/2 -top-3">
285
- <button
286
- onClick={() => addLessonAfter(index, lesson.id)}
287
- className="w-6 h-6 flex items-center justify-center bg-blue-100 text-blue-600 rounded hover:bg-blue-200 shadow-sm text-sm font-semibold cursor-pointer"
288
- >
289
- +
290
- </button>
290
+ <Tooltip>
291
+ <TooltipTrigger asChild>
292
+ <button
293
+ onClick={() => addLessonAfter(index, lesson.id)}
294
+ className="w-6 h-6 flex items-center justify-center bg-blue-100 text-blue-600 rounded hover:bg-blue-200 shadow-sm text-sm font-semibold cursor-pointer"
295
+ >
296
+ +
297
+ </button>
298
+ </TooltipTrigger>
299
+ <TooltipContent>
300
+ <p>{t("tooltips.addLessonHere")}</p>
301
+ </TooltipContent>
302
+ </Tooltip>
291
303
  </div>
292
304
  </div>
293
305
  )}
@@ -1,10 +1,11 @@
1
- import { useEffect, useRef, useState } from "react"
2
- import useStore from "../../utils/store"
3
- import FileUploader from "../FileUploader"
4
- import { SVGS } from "../../assets/svgs"
5
- import { Message } from "../Message"
6
- import { FileCard } from "../FileCard"
7
- import { useTranslation } from "react-i18next"
1
+ import { useEffect, useRef, useState } from "react";
2
+ import useStore from "../../utils/store";
3
+ import FileUploader from "../FileUploader";
4
+ import { SVGS } from "../../assets/svgs";
5
+ import { Message } from "../Message";
6
+ import { FileCard } from "../FileCard";
7
+ import { useTranslation } from "react-i18next";
8
+ import { Tooltip, TooltipTrigger, TooltipContent } from "../ui/tooltip";
8
9
 
9
10
  export const Sidebar = ({
10
11
  // messages,
@@ -12,19 +13,19 @@ export const Sidebar = ({
12
13
  handleSubmit,
13
14
  }: {
14
15
  // messages: TMessage[]
15
- sendPrompt: (prompt: string) => void
16
- handleSubmit: () => void
16
+ sendPrompt: (prompt: string) => void;
17
+ handleSubmit: () => void;
17
18
  }) => {
18
- const { t } = useTranslation()
19
+ const { t } = useTranslation();
19
20
 
20
- const sidebarRef = useRef<HTMLDivElement>(null)
21
- const inputRef = useRef<HTMLTextAreaElement>(null)
22
- const uploadedFiles = useStore((state) => state.uploadedFiles)
23
- const setUploadedFiles = useStore((state) => state.setUploadedFiles)
24
- const messages = useStore((state) => state.messages)
25
- const [showBubble, setShowBubble] = useState(true)
21
+ const sidebarRef = useRef<HTMLDivElement>(null);
22
+ const inputRef = useRef<HTMLTextAreaElement>(null);
23
+ const uploadedFiles = useStore((state) => state.uploadedFiles);
24
+ const setUploadedFiles = useStore((state) => state.setUploadedFiles);
25
+ const messages = useStore((state) => state.messages);
26
+ const [showBubble, setShowBubble] = useState(true);
26
27
 
27
- const [isOpen, setIsOpen] = useState(false)
28
+ const [isOpen, setIsOpen] = useState(false);
28
29
 
29
30
  useEffect(() => {
30
31
  const handleClickOutside = (e: MouseEvent) => {
@@ -33,12 +34,12 @@ export const Sidebar = ({
33
34
  sidebarRef.current &&
34
35
  !sidebarRef.current.contains(e.target as Node)
35
36
  ) {
36
- setIsOpen(false)
37
+ setIsOpen(false);
37
38
  }
38
- }
39
- document.addEventListener("mousedown", handleClickOutside)
40
- return () => document.removeEventListener("mousedown", handleClickOutside)
41
- }, [isOpen])
39
+ };
40
+ document.addEventListener("mousedown", handleClickOutside);
41
+ return () => document.removeEventListener("mousedown", handleClickOutside);
42
+ }, [isOpen]);
42
43
  return (
43
44
  <>
44
45
  {!isOpen && (
@@ -98,9 +99,9 @@ export const Sidebar = ({
98
99
  key={index}
99
100
  file={file}
100
101
  handleRemove={() => {
101
- const newFiles = [...uploadedFiles]
102
- newFiles.splice(index, 1)
103
- setUploadedFiles(newFiles)
102
+ const newFiles = [...uploadedFiles];
103
+ newFiles.splice(index, 1);
104
+ setUploadedFiles(newFiles);
104
105
  }}
105
106
  />
106
107
  ))}
@@ -113,40 +114,47 @@ export const Sidebar = ({
113
114
  placeholder={t("sidebar.howCanLearnPackHelpYou")}
114
115
  onKeyUp={(e) => {
115
116
  if (e.key === "Enter" && !e.shiftKey) {
116
- e.preventDefault()
117
- const val = inputRef.current?.value.trim() || ""
117
+ e.preventDefault();
118
+ const val = inputRef.current?.value.trim() || "";
118
119
 
119
120
  if (val.toLowerCase() === "ok") {
120
- handleSubmit()
121
+ handleSubmit();
121
122
  } else {
122
- sendPrompt(val)
123
+ sendPrompt(val);
123
124
  }
124
125
 
125
- inputRef.current!.value = ""
126
+ inputRef.current!.value = "";
126
127
  }
127
128
  }}
128
129
  />
129
130
  <div className="absolute bottom-2 right-2 flex gap-1 items-center">
130
131
  <FileUploader />
131
- <button
132
- className="cursor-pointer blue-on-hover flex items-center justify-center w-6 h-6"
133
- onClick={() => {
134
- if (inputRef.current?.value) {
135
- const val = inputRef.current?.value.trim() || ""
136
- if (val.toLowerCase() === "ok") {
137
- handleSubmit()
138
- } else {
139
- sendPrompt(val)
140
- }
141
- inputRef.current!.value = ""
142
- }
143
- }}
144
- >
145
- {SVGS.send}
146
- </button>
132
+ <Tooltip>
133
+ <TooltipTrigger asChild>
134
+ <button
135
+ className="cursor-pointer blue-on-hover flex items-center justify-center w-6 h-6"
136
+ onClick={() => {
137
+ if (inputRef.current?.value) {
138
+ const val = inputRef.current?.value.trim() || "";
139
+ if (val.toLowerCase() === "ok") {
140
+ handleSubmit();
141
+ } else {
142
+ sendPrompt(val);
143
+ }
144
+ inputRef.current!.value = "";
145
+ }
146
+ }}
147
+ >
148
+ {SVGS.send}
149
+ </button>
150
+ </TooltipTrigger>
151
+ <TooltipContent>
152
+ <p>{t("tooltips.send")}</p>
153
+ </TooltipContent>
154
+ </Tooltip>
147
155
  </div>
148
156
  </div>
149
157
  </div>
150
158
  </>
151
- )
152
- }
159
+ );
160
+ };
@@ -0,0 +1,31 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const TooltipProvider = TooltipPrimitive.Provider
9
+
10
+ const Tooltip = TooltipPrimitive.Root
11
+
12
+ const TooltipTrigger = TooltipPrimitive.Trigger
13
+
14
+ const TooltipContent = React.forwardRef<
15
+ React.ElementRef<typeof TooltipPrimitive.Content>,
16
+ React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
17
+ >(({ className, sideOffset = 4, ...props }, ref) => (
18
+ <TooltipPrimitive.Content
19
+ ref={ref}
20
+ sideOffset={sideOffset}
21
+ className={cn(
22
+ "z-50 overflow-hidden rounded-md border border-gray-200 bg-white px-3 py-1.5 text-sm text-gray-800 shadow-lg animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
23
+ className
24
+ )}
25
+ {...props}
26
+ />
27
+ ))
28
+ TooltipContent.displayName = TooltipPrimitive.Content.displayName
29
+
30
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
31
+
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -68,7 +68,19 @@
68
68
  },
69
69
  "youtube": {
70
70
  "description": "Share a Youtube link",
71
- "placeholder": "Paste your link here…"
71
+ "placeholder": "Paste your YouTube link here…",
72
+ "invalidUrl": "Please enter a valid YouTube URL",
73
+ "notAvailable": "Transcript not available for this video.",
74
+ "tooManyRequests": "Too many requests. Please try again later.",
75
+ "serviceUnavailable": "Service temporarily unavailable. Please try again later.",
76
+ "fetchError": "Failed to fetch transcript",
77
+ "enterUrl": "Please enter a URL",
78
+ "notAvailableReasons": {
79
+ "title": "Please check the following:",
80
+ "captions": "The video has captions/subtitles enabled.",
81
+ "privacy": "The video is public and not restricted.",
82
+ "region": "The video is not region-locked or blocked."
83
+ }
72
84
  }
73
85
  },
74
86
  "loader": {
@@ -135,5 +147,16 @@
135
147
  "turnstileModal": {
136
148
  "title": "We need to verify you are a human, wait a moment...",
137
149
  "error": "Error verifying you are a human, please try again, if the problem persists, use a different browser."
150
+ },
151
+ "tooltips": {
152
+ "lockLesson": "Lock lesson",
153
+ "unlockLesson": "Unlock lesson",
154
+ "editLesson": "Edit lesson",
155
+ "deleteLesson": "Delete lesson",
156
+ "cannotEditLocked": "Cannot edit locked lesson",
157
+ "cannotDeleteLocked": "Cannot delete locked lesson",
158
+ "addLessonHere": "Add lesson here",
159
+ "attachFiles": "Attach files",
160
+ "send": "Send"
138
161
  }
139
162
  }