|
1 | 1 | "use client"; |
2 | 2 |
|
3 | | -import { IconBrandYoutube } from "@tabler/icons-react"; |
4 | | -import { type Tag, TagInput } from "emblor"; |
5 | | -import { Loader2 } from "lucide-react"; |
6 | | -import { motion, type Variants } from "motion/react"; |
7 | 3 | import { useParams, useRouter } from "next/navigation"; |
8 | | -import { useTranslations } from "next-intl"; |
9 | | -import { useState } from "react"; |
10 | | -import { toast } from "sonner"; |
11 | | -import { Button } from "@/components/ui/button"; |
12 | | -import { |
13 | | - Card, |
14 | | - CardContent, |
15 | | - CardDescription, |
16 | | - CardFooter, |
17 | | - CardHeader, |
18 | | - CardTitle, |
19 | | -} from "@/components/ui/card"; |
20 | | -import { Label } from "@/components/ui/label"; |
| 4 | +import { useEffect } from "react"; |
21 | 5 |
|
22 | | -// YouTube video ID validation regex |
23 | | -const youtubeRegex = |
24 | | - /^(https:\/\/)?(www\.)?(youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})$/; |
25 | | - |
26 | | -export default function YouTubeVideoAdder() { |
27 | | - const t = useTranslations("add_youtube"); |
| 6 | +export default function YouTubeRedirect() { |
28 | 7 | const params = useParams(); |
29 | 8 | const router = useRouter(); |
30 | 9 | const search_space_id = params.search_space_id as string; |
31 | 10 |
|
32 | | - const [videoTags, setVideoTags] = useState<Tag[]>([]); |
33 | | - const [activeTagIndex, setActiveTagIndex] = useState<number | null>(null); |
34 | | - const [isSubmitting, setIsSubmitting] = useState(false); |
35 | | - const [error, setError] = useState<string | null>(null); |
36 | | - |
37 | | - // Function to validate a YouTube URL |
38 | | - const isValidYoutubeUrl = (url: string): boolean => { |
39 | | - return youtubeRegex.test(url); |
40 | | - }; |
41 | | - |
42 | | - // Function to extract video ID from URL |
43 | | - const extractVideoId = (url: string): string | null => { |
44 | | - const match = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/); |
45 | | - return match ? match[1] : null; |
46 | | - }; |
47 | | - |
48 | | - // Function to handle video URL submission |
49 | | - const handleSubmit = async () => { |
50 | | - // Validate that we have at least one video URL |
51 | | - if (videoTags.length === 0) { |
52 | | - setError(t("error_no_video")); |
53 | | - return; |
54 | | - } |
55 | | - |
56 | | - // Validate all URLs |
57 | | - const invalidUrls = videoTags.filter((tag) => !isValidYoutubeUrl(tag.text)); |
58 | | - if (invalidUrls.length > 0) { |
59 | | - setError(t("error_invalid_urls", { urls: invalidUrls.map((tag) => tag.text).join(", ") })); |
60 | | - return; |
61 | | - } |
62 | | - |
63 | | - setError(null); |
64 | | - setIsSubmitting(true); |
65 | | - |
66 | | - try { |
67 | | - toast(t("processing_toast"), { |
68 | | - description: t("processing_toast_desc"), |
69 | | - }); |
70 | | - |
71 | | - // Extract URLs from tags |
72 | | - const videoUrls = videoTags.map((tag) => tag.text); |
73 | | - |
74 | | - // Make API call to backend |
75 | | - const response = await fetch( |
76 | | - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents`, |
77 | | - { |
78 | | - method: "POST", |
79 | | - headers: { |
80 | | - "Content-Type": "application/json", |
81 | | - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, |
82 | | - }, |
83 | | - body: JSON.stringify({ |
84 | | - document_type: "YOUTUBE_VIDEO", |
85 | | - content: videoUrls, |
86 | | - search_space_id: parseInt(search_space_id), |
87 | | - }), |
88 | | - } |
89 | | - ); |
90 | | - |
91 | | - if (!response.ok) { |
92 | | - throw new Error("Failed to process YouTube videos"); |
93 | | - } |
94 | | - |
95 | | - await response.json(); |
96 | | - |
97 | | - toast(t("success_toast"), { |
98 | | - description: t("success_toast_desc"), |
99 | | - }); |
100 | | - |
101 | | - // Redirect to documents page |
102 | | - router.push(`/dashboard/${search_space_id}/documents`); |
103 | | - } catch (error: any) { |
104 | | - setError(error.message || t("error_generic")); |
105 | | - toast(t("error_toast"), { |
106 | | - description: `${t("error_toast_desc")}: ${error.message}`, |
107 | | - }); |
108 | | - } finally { |
109 | | - setIsSubmitting(false); |
110 | | - } |
111 | | - }; |
112 | | - |
113 | | - // Function to add a new video URL tag |
114 | | - const handleAddTag = (text: string) => { |
115 | | - // Basic URL validation |
116 | | - if (!isValidYoutubeUrl(text)) { |
117 | | - toast(t("invalid_url_toast"), { |
118 | | - description: t("invalid_url_toast_desc"), |
119 | | - }); |
120 | | - return; |
121 | | - } |
122 | | - |
123 | | - // Check for duplicates |
124 | | - if (videoTags.some((tag) => tag.text === text)) { |
125 | | - toast(t("duplicate_url_toast"), { |
126 | | - description: t("duplicate_url_toast_desc"), |
127 | | - }); |
128 | | - return; |
129 | | - } |
130 | | - |
131 | | - // Add the new tag |
132 | | - const newTag: Tag = { |
133 | | - id: Date.now().toString(), |
134 | | - text: text, |
135 | | - }; |
136 | | - |
137 | | - setVideoTags([...videoTags, newTag]); |
138 | | - }; |
139 | | - |
140 | | - // Animation variants |
141 | | - const containerVariants: Variants = { |
142 | | - hidden: { opacity: 0 }, |
143 | | - visible: { |
144 | | - opacity: 1, |
145 | | - transition: { |
146 | | - staggerChildren: 0.1, |
147 | | - }, |
148 | | - }, |
149 | | - }; |
150 | | - |
151 | | - const itemVariants: Variants = { |
152 | | - hidden: { y: 20, opacity: 0 }, |
153 | | - visible: { |
154 | | - y: 0, |
155 | | - opacity: 1, |
156 | | - transition: { |
157 | | - type: "spring", |
158 | | - stiffness: 300, |
159 | | - damping: 24, |
160 | | - }, |
161 | | - }, |
162 | | - }; |
163 | | - |
164 | | - return ( |
165 | | - <div className="container mx-auto py-8"> |
166 | | - <motion.div initial="hidden" animate="visible" variants={containerVariants}> |
167 | | - <Card className="max-w-2xl mx-auto"> |
168 | | - <motion.div variants={itemVariants}> |
169 | | - <CardHeader> |
170 | | - <CardTitle className="flex items-center gap-2"> |
171 | | - <IconBrandYoutube className="h-5 w-5" /> |
172 | | - {t("title")} |
173 | | - </CardTitle> |
174 | | - <CardDescription>{t("subtitle")}</CardDescription> |
175 | | - </CardHeader> |
176 | | - </motion.div> |
177 | | - |
178 | | - <motion.div variants={itemVariants}> |
179 | | - <CardContent> |
180 | | - <div className="space-y-4"> |
181 | | - <div className="space-y-2"> |
182 | | - <Label htmlFor="video-input">{t("label")}</Label> |
183 | | - <TagInput |
184 | | - id="video-input" |
185 | | - tags={videoTags} |
186 | | - setTags={setVideoTags} |
187 | | - placeholder={t("placeholder")} |
188 | | - onAddTag={handleAddTag} |
189 | | - styleClasses={{ |
190 | | - inlineTagsContainer: |
191 | | - "border-input rounded-lg bg-background shadow-sm shadow-black/5 transition-shadow focus-within:border-ring focus-within:outline-none focus-within:ring-[3px] focus-within:ring-ring/20 p-1 gap-1", |
192 | | - input: "w-full min-w-[80px] focus-visible:outline-none shadow-none px-2 h-7", |
193 | | - tag: { |
194 | | - body: "h-7 relative bg-background border border-input hover:bg-background rounded-md font-medium text-xs ps-2 pe-7 flex", |
195 | | - closeButton: |
196 | | - "absolute -inset-y-px -end-px p-0 rounded-e-lg flex size-7 transition-colors outline-0 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring/70 text-muted-foreground/80 hover:text-foreground", |
197 | | - }, |
198 | | - }} |
199 | | - activeTagIndex={activeTagIndex} |
200 | | - setActiveTagIndex={setActiveTagIndex} |
201 | | - /> |
202 | | - <p className="text-xs text-muted-foreground mt-1">{t("hint")}</p> |
203 | | - </div> |
204 | | - |
205 | | - {error && ( |
206 | | - <motion.div |
207 | | - className="text-sm text-red-500 mt-2" |
208 | | - initial={{ opacity: 0, scale: 0.9 }} |
209 | | - animate={{ opacity: 1, scale: 1 }} |
210 | | - transition={{ type: "spring", stiffness: 500, damping: 30 }} |
211 | | - > |
212 | | - {error} |
213 | | - </motion.div> |
214 | | - )} |
215 | | - |
216 | | - <motion.div variants={itemVariants} className="bg-muted/50 rounded-lg p-4 text-sm"> |
217 | | - <h4 className="font-medium mb-2">{t("tips_title")}</h4> |
218 | | - <ul className="list-disc pl-5 space-y-1 text-muted-foreground"> |
219 | | - <li>{t("tip_1")}</li> |
220 | | - <li>{t("tip_2")}</li> |
221 | | - <li>{t("tip_3")}</li> |
222 | | - <li>{t("tip_4")}</li> |
223 | | - </ul> |
224 | | - </motion.div> |
225 | | - |
226 | | - {videoTags.length > 0 && ( |
227 | | - <motion.div variants={itemVariants} className="mt-4 space-y-2"> |
228 | | - <h4 className="font-medium">{t("preview")}:</h4> |
229 | | - <div className="grid grid-cols-1 gap-3"> |
230 | | - {videoTags.map((tag, index) => { |
231 | | - const videoId = extractVideoId(tag.text); |
232 | | - return videoId ? ( |
233 | | - <motion.div |
234 | | - key={tag.id} |
235 | | - initial={{ opacity: 0, y: 10 }} |
236 | | - animate={{ opacity: 1, y: 0 }} |
237 | | - transition={{ delay: index * 0.1 }} |
238 | | - className="relative aspect-video rounded-lg overflow-hidden border" |
239 | | - > |
240 | | - <iframe |
241 | | - width="100%" |
242 | | - height="100%" |
243 | | - src={`https://www.youtube.com/embed/${videoId}`} |
244 | | - title="YouTube video player" |
245 | | - allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" |
246 | | - allowFullScreen |
247 | | - ></iframe> |
248 | | - </motion.div> |
249 | | - ) : null; |
250 | | - })} |
251 | | - </div> |
252 | | - </motion.div> |
253 | | - )} |
254 | | - </div> |
255 | | - </CardContent> |
256 | | - </motion.div> |
| 11 | + useEffect(() => { |
| 12 | + router.replace(`/dashboard/${search_space_id}/sources/add?tab=youtube`); |
| 13 | + }, [search_space_id, router]); |
257 | 14 |
|
258 | | - <motion.div variants={itemVariants}> |
259 | | - <CardFooter className="flex justify-between"> |
260 | | - <Button |
261 | | - variant="outline" |
262 | | - onClick={() => router.push(`/dashboard/${search_space_id}/documents`)} |
263 | | - > |
264 | | - {t("cancel")} |
265 | | - </Button> |
266 | | - <Button |
267 | | - onClick={handleSubmit} |
268 | | - disabled={isSubmitting || videoTags.length === 0} |
269 | | - className="relative overflow-hidden" |
270 | | - > |
271 | | - {isSubmitting ? ( |
272 | | - <> |
273 | | - <Loader2 className="mr-2 h-4 w-4 animate-spin" /> |
274 | | - {t("processing")} |
275 | | - </> |
276 | | - ) : ( |
277 | | - <> |
278 | | - <motion.span |
279 | | - initial={{ x: -5, opacity: 0 }} |
280 | | - animate={{ x: 0, opacity: 1 }} |
281 | | - transition={{ delay: 0.2 }} |
282 | | - className="mr-2" |
283 | | - > |
284 | | - <IconBrandYoutube className="h-4 w-4" /> |
285 | | - </motion.span> |
286 | | - {t("submit")} |
287 | | - </> |
288 | | - )} |
289 | | - <motion.div |
290 | | - className="absolute inset-0 bg-primary/10" |
291 | | - initial={{ x: "-100%" }} |
292 | | - animate={isSubmitting ? { x: "0%" } : { x: "-100%" }} |
293 | | - transition={{ duration: 0.5, ease: "easeInOut" }} |
294 | | - /> |
295 | | - </Button> |
296 | | - </CardFooter> |
297 | | - </motion.div> |
298 | | - </Card> |
299 | | - </motion.div> |
300 | | - </div> |
301 | | - ); |
| 15 | + return null; |
302 | 16 | } |
0 commit comments