Shivam commited on
Commit
987282d
·
1 Parent(s): ae19567

Committing all changes

Browse files
components/chat/ChatPanel.tsx ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+ import { FC, useEffect, useRef, useState } from "react"
3
+ import { Socket } from "socket.io-client"
4
+ import { ClientToServerEvents, ServerToClientEvents } from "../../lib/socket"
5
+
6
+ type ChatMessage = {
7
+ id: string
8
+ userId: string
9
+ name: string
10
+ text: string
11
+ ts: number
12
+ }
13
+
14
+ interface Props {
15
+ socket: Socket<ServerToClientEvents, ClientToServerEvents>
16
+ className?: string
17
+ }
18
+
19
+ const ChatPanel: FC<Props> = ({ socket, className }) => {
20
+ const [messages, _setMessages] = useState<ChatMessage[]>([])
21
+ const [text, setText] = useState("")
22
+ const messagesRef = useRef(messages)
23
+ const setMessages = (m: ChatMessage[]) => {
24
+ messagesRef.current = m
25
+ _setMessages(m)
26
+ }
27
+
28
+ useEffect(() => {
29
+ const onHistory = (history: ChatMessage[]) => {
30
+ setMessages(history)
31
+ }
32
+ const onNew = (msg: ChatMessage) => {
33
+ setMessages([...messagesRef.current, msg].slice(-200))
34
+ }
35
+
36
+ socket.on("chatHistory", onHistory)
37
+ socket.on("chatNew", onNew)
38
+ return () => {
39
+ socket.off("chatHistory", onHistory)
40
+ socket.off("chatNew", onNew)
41
+ }
42
+ }, [socket])
43
+
44
+ const send = () => {
45
+ const trimmed = text.trim()
46
+ if (!trimmed) return
47
+ socket.emit("chatMessage", trimmed)
48
+ setText("")
49
+ }
50
+
51
+ return (
52
+ <div className={className ?? "flex flex-col h-64 border rounded-md"}>
53
+ <div className="flex-1 overflow-y-auto p-2 space-y-2 bg-neutral-900/30">
54
+ {messages.map((m) => (
55
+ <div key={m.id} className="text-sm">
56
+ <span className="font-semibold">{m.name}</span>
57
+ <span className="opacity-60"> • {new Date(m.ts).toLocaleTimeString()}</span>
58
+ <div className="break-words">{m.text}</div>
59
+ </div>
60
+ ))}
61
+ {messages.length === 0 && <div className="opacity-60 text-sm">No messages yet</div>}
62
+ </div>
63
+ <div className="p-2 flex gap-2">
64
+ <input
65
+ className="input flex-1 bg-neutral-800 p-2 rounded-md outline-none"
66
+ placeholder="Type a message…"
67
+ value={text}
68
+ onChange={(e) => setText(e.target.value)}
69
+ onKeyDown={(e) => {
70
+ if (e.key === "Enter") send()
71
+ }}
72
+ />
73
+ <button className="btn bg-primary-700 hover:bg-primary-600 px-3 rounded-md" onClick={send}>
74
+ Send
75
+ </button>
76
+ </div>
77
+ </div>
78
+ )
79
+ }
80
+
81
+ export default ChatPanel
components/search/YoutubeSearch.tsx ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+ import { FC, useState } from "react"
3
+ import { Socket } from "socket.io-client"
4
+ import { ClientToServerEvents, ServerToClientEvents } from "../../lib/socket"
5
+
6
+ type Result = {
7
+ id: string
8
+ title: string
9
+ url: string
10
+ duration?: number
11
+ thumbnails?: { url: string; width?: number; height?: number }[]
12
+ }
13
+
14
+ interface Props {
15
+ socket: Socket<ServerToClientEvents, ClientToServerEvents> | null
16
+ }
17
+
18
+ const YoutubeSearch: FC<Props> = ({ socket }) => {
19
+ const [q, setQ] = useState("")
20
+ const [loading, setLoading] = useState(false)
21
+ const [results, setResults] = useState<Result[]>([])
22
+ const [error, setError] = useState<string | null>(null)
23
+
24
+ const search = async () => {
25
+ const query = q.trim()
26
+ if (!query) return
27
+ setLoading(true)
28
+ setError(null)
29
+ try {
30
+ const r = await fetch(`/api/search?q=${encodeURIComponent(query)}&limit=8`)
31
+ const data = await r.json()
32
+ if (!r.ok) throw new Error(data?.error || "Failed to search")
33
+ setResults(data.results || [])
34
+ } catch (e: any) {
35
+ setError(e.message || "Failed to search")
36
+ } finally {
37
+ setLoading(false)
38
+ }
39
+ }
40
+
41
+ return (
42
+ <div className="flex flex-col gap-2">
43
+ <div className="flex gap-2">
44
+ <input
45
+ className="input flex-1 bg-neutral-800 p-2 rounded-md outline-none"
46
+ placeholder="Search YouTube (e.g., titanium sia)"
47
+ value={q}
48
+ onChange={(e) => setQ(e.target.value)}
49
+ onKeyDown={(e) => {
50
+ if (e.key === "Enter") search()
51
+ }}
52
+ />
53
+ <button onClick={search} className="btn bg-primary-700 hover:bg-primary-600 px-3 rounded-md">
54
+ {loading ? "Searching…" : "Search"}
55
+ </button>
56
+ </div>
57
+
58
+ {error && <div className="text-red-400 text-sm">{error}</div>}
59
+
60
+ <div className="grid gap-2">
61
+ {results.map((r) => (
62
+ <div key={r.id} className="flex items-center gap-3 p-2 rounded-md border">
63
+ {r.thumbnails?.[0]?.url && (
64
+ // eslint-disable-next-line @next/next/no-img-element
65
+ <img src={r.thumbnails[0].url} alt="" className="w-16 h-9 object-cover rounded-sm" />
66
+ )}
67
+ <div className="flex-1 overflow-hidden">
68
+ <div className="truncate">{r.title}</div>
69
+ <div className="opacity-60 text-xs truncate">{r.url}</div>
70
+ </div>
71
+ <button
72
+ className="btn bg-primary-800 hover:bg-primary-700 px-3 rounded-md"
73
+ onClick={() => socket?.emit("playUrl", r.url)}
74
+ >
75
+ Play
76
+ </button>
77
+ </div>
78
+ ))}
79
+ </div>
80
+ </div>
81
+ )
82
+ }
83
+
84
+ export default YoutubeSearch