copilot-swe-agent[bot] ArnavSingh76533 commited on
Commit
5877a31
·
1 Parent(s): 075d8be

Implement name handling and YouTube search close button

Browse files

Co-authored-by: ArnavSingh76533 <160649079+ArnavSingh76533@users.noreply.github.com>

components/Room.tsx CHANGED
@@ -15,6 +15,7 @@ import InputUrl from "./input/InputUrl"
15
  import UserList from "./user/UserList"
16
  import ChatPanel from "./chat/ChatPanel"
17
  import YoutubeSearch from "./search/YoutubeSearch"
 
18
 
19
  interface Props {
20
  id: string
@@ -29,6 +30,7 @@ const Room: FC<Props> = ({ id }) => {
29
  ClientToServerEvents
30
  > | null>(null)
31
  const [url, setUrl] = useState("")
 
32
 
33
  useEffect(() => {
34
  fetch("/api/socketio").finally(() => {
@@ -40,6 +42,12 @@ const Room: FC<Props> = ({ id }) => {
40
  ? localStorage.getItem("userName") || undefined
41
  : undefined
42
 
 
 
 
 
 
 
43
  // Get room metadata from sessionStorage (set during creation)
44
  const roomKey = `room_${id}_meta`
45
  const roomMeta = typeof window !== "undefined" && sessionStorage.getItem(roomKey)
@@ -72,6 +80,33 @@ const Room: FC<Props> = ({ id }) => {
72
  setTimeout(connectionCheck, 100)
73
  }
74
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  if (!connected || socket === null) {
76
  if (!connecting) {
77
  connecting = true
 
15
  import UserList from "./user/UserList"
16
  import ChatPanel from "./chat/ChatPanel"
17
  import YoutubeSearch from "./search/YoutubeSearch"
18
+ import NameModal from "./modal/NameModal"
19
 
20
  interface Props {
21
  id: string
 
30
  ClientToServerEvents
31
  > | null>(null)
32
  const [url, setUrl] = useState("")
33
+ const [showNameModal, setShowNameModal] = useState(false)
34
 
35
  useEffect(() => {
36
  fetch("/api/socketio").finally(() => {
 
42
  ? localStorage.getItem("userName") || undefined
43
  : undefined
44
 
45
+ // Show modal if no userName
46
+ if (!userName || !userName.trim()) {
47
+ setShowNameModal(true)
48
+ return
49
+ }
50
+
51
  // Get room metadata from sessionStorage (set during creation)
52
  const roomKey = `room_${id}_meta`
53
  const roomMeta = typeof window !== "undefined" && sessionStorage.getItem(roomKey)
 
80
  setTimeout(connectionCheck, 100)
81
  }
82
 
83
+ const handleNameSubmit = (name: string) => {
84
+ // Save to localStorage
85
+ if (typeof window !== "undefined") {
86
+ localStorage.setItem("userName", name)
87
+ }
88
+ setShowNameModal(false)
89
+
90
+ // Get room metadata from sessionStorage (set during creation)
91
+ const roomKey = `room_${id}_meta`
92
+ const roomMeta = typeof window !== "undefined" && sessionStorage.getItem(roomKey)
93
+ ? JSON.parse(sessionStorage.getItem(roomKey) || "{}")
94
+ : {}
95
+
96
+ const isPublic = roomMeta.isPublic
97
+
98
+ // Create socket connection with the name
99
+ const newSocket = createClientSocket(id, name, isPublic)
100
+ newSocket.on("connect", () => {
101
+ setConnected(true)
102
+ })
103
+ setSocket(newSocket)
104
+ }
105
+
106
+ if (showNameModal) {
107
+ return <NameModal show={true} onSubmit={handleNameSubmit} />
108
+ }
109
+
110
  if (!connected || socket === null) {
111
  if (!connecting) {
112
  connecting = true
components/modal/NameModal.tsx ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { FC, useState } from "react"
2
+ import Modal from "./Modal"
3
+ import InputText from "../input/InputText"
4
+ import Button from "../action/Button"
5
+
6
+ interface Props {
7
+ show: boolean
8
+ onSubmit: (name: string) => void
9
+ }
10
+
11
+ const NameModal: FC<Props> = ({ show, onSubmit }) => {
12
+ const [name, setName] = useState("")
13
+ const [error, setError] = useState("")
14
+
15
+ const handleSubmit = () => {
16
+ const trimmedName = name.trim()
17
+ if (!trimmedName) {
18
+ setError("Please enter your name")
19
+ return
20
+ }
21
+ if (trimmedName.length < 2) {
22
+ setError("Name must be at least 2 characters")
23
+ return
24
+ }
25
+ if (trimmedName.length > 30) {
26
+ setError("Name must be less than 30 characters")
27
+ return
28
+ }
29
+ onSubmit(trimmedName)
30
+ }
31
+
32
+ return (
33
+ <Modal title="Enter Your Name" show={show} close={() => {}}>
34
+ <div className="space-y-4">
35
+ <p className="text-dark-300">
36
+ Please enter your name to join this room. This will be your display name visible to other participants.
37
+ </p>
38
+ <div>
39
+ <InputText
40
+ value={name}
41
+ placeholder="Your display name"
42
+ onChange={(value) => {
43
+ setName(value)
44
+ setError("")
45
+ }}
46
+ required
47
+ />
48
+ {error && (
49
+ <p className="text-red-500 text-sm mt-2">{error}</p>
50
+ )}
51
+ </div>
52
+ <div className="flex justify-end pt-2">
53
+ <Button
54
+ tooltip="Continue with this name"
55
+ className="px-6 py-2.5 font-medium"
56
+ actionClasses="bg-primary-600 hover:bg-primary-700 active:bg-primary-800"
57
+ onClick={handleSubmit}
58
+ >
59
+ Continue
60
+ </Button>
61
+ </div>
62
+ </div>
63
+ </Modal>
64
+ )
65
+ }
66
+
67
+ export default NameModal
components/search/YoutubeSearch.tsx CHANGED
@@ -104,6 +104,10 @@ const YoutubeSearch: FC<Props> = ({ socket }) => {
104
 
105
  const playNow = (url: string) => socket?.emit("playUrl", url)
106
  const addToPlaylist = (url: string) => socket?.emit("addToPlaylist", url)
 
 
 
 
107
 
108
  return (
109
  <div className="flex flex-col gap-3 bg-dark-900 border border-dark-700/50 rounded-xl p-4 shadow-lg">
@@ -130,46 +134,62 @@ const YoutubeSearch: FC<Props> = ({ socket }) => {
130
 
131
  {error && <div className="text-red-400 text-sm bg-red-900/20 border border-red-700/30 rounded-lg p-2">{error}</div>}
132
 
133
- <div className="grid gap-2 max-h-96 overflow-y-auto">
134
- {results.map((r) => (
135
- <div
136
- key={r.id}
137
- className="grid grid-cols-[auto_1fr_auto_auto] items-center gap-3 p-3 rounded-lg border border-dark-700/50 bg-dark-800/50 hover:bg-dark-800 transition-all duration-200"
138
- >
139
- {/* Thumb */}
140
- {r.thumbnails?.[0]?.url ? (
141
- // eslint-disable-next-line @next/next/no-img-element
142
- <img src={r.thumbnails[0].url} alt="" className="w-20 h-12 object-cover rounded-md border border-dark-700/30" />
143
- ) : (
144
- <div className="w-20 h-12 bg-dark-700 rounded-md border border-dark-700/30" />
145
- )}
146
-
147
- {/* Info */}
148
- <div className="min-w-0">
149
- <div className="truncate font-medium text-dark-200">{r.title}</div>
150
- <div className="text-dark-500 text-xs truncate">{r.url}</div>
151
- </div>
152
-
153
- {/* Add (small) */}
154
  <button
155
- className={`btn bg-accent-600 hover:bg-accent-700 active:bg-accent-800 px-3 py-1.5 rounded-lg text-xs justify-center font-medium transition-all duration-200 ${ADD_BTN_WIDTH}`}
156
- onClick={() => addToPlaylist(r.url)}
157
- title="Add to playlist"
158
  >
159
- Add
160
- </button>
161
-
162
- {/* Play (aligned under Search button) */}
163
- <button
164
- className={`btn bg-primary-600 hover:bg-primary-700 active:bg-primary-800 px-3 py-1.5 rounded-lg justify-center font-medium transition-all duration-200 shadow-md hover:shadow-glow ${ACTION_BTN_WIDTH}`}
165
- onClick={() => playNow(r.url)}
166
- title="Play now"
167
- >
168
- Play
169
  </button>
170
  </div>
171
- ))}
172
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  </div>
174
  )
175
  }
 
104
 
105
  const playNow = (url: string) => socket?.emit("playUrl", url)
106
  const addToPlaylist = (url: string) => socket?.emit("addToPlaylist", url)
107
+ const closeResults = () => {
108
+ setResults([])
109
+ setError(null)
110
+ }
111
 
112
  return (
113
  <div className="flex flex-col gap-3 bg-dark-900 border border-dark-700/50 rounded-xl p-4 shadow-lg">
 
134
 
135
  {error && <div className="text-red-400 text-sm bg-red-900/20 border border-red-700/30 rounded-lg p-2">{error}</div>}
136
 
137
+ {results.length > 0 && (
138
+ <div className="space-y-2">
139
+ {/* Close button for results */}
140
+ <div className="flex justify-between items-center">
141
+ <span className="text-sm text-dark-400">{results.length} results</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  <button
143
+ onClick={closeResults}
144
+ className="text-sm text-dark-400 hover:text-dark-200 underline"
145
+ title="Close search results"
146
  >
147
+ Close
 
 
 
 
 
 
 
 
 
148
  </button>
149
  </div>
150
+
151
+ <div className="grid gap-2 max-h-96 overflow-y-auto">
152
+ {results.map((r) => (
153
+ <div
154
+ key={r.id}
155
+ className="grid grid-cols-[auto_1fr_auto_auto] items-center gap-3 p-3 rounded-lg border border-dark-700/50 bg-dark-800/50 hover:bg-dark-800 transition-all duration-200"
156
+ >
157
+ {/* Thumb */}
158
+ {r.thumbnails?.[0]?.url ? (
159
+ // eslint-disable-next-line @next/next/no-img-element
160
+ <img src={r.thumbnails[0].url} alt="" className="w-20 h-12 object-cover rounded-md border border-dark-700/30" />
161
+ ) : (
162
+ <div className="w-20 h-12 bg-dark-700 rounded-md border border-dark-700/30" />
163
+ )}
164
+
165
+ {/* Info */}
166
+ <div className="min-w-0">
167
+ <div className="truncate font-medium text-dark-200">{r.title}</div>
168
+ <div className="text-dark-500 text-xs truncate">{r.url}</div>
169
+ </div>
170
+
171
+ {/* Add (small) */}
172
+ <button
173
+ className={`btn bg-accent-600 hover:bg-accent-700 active:bg-accent-800 px-3 py-1.5 rounded-lg text-xs justify-center font-medium transition-all duration-200 ${ADD_BTN_WIDTH}`}
174
+ onClick={() => addToPlaylist(r.url)}
175
+ title="Add to playlist"
176
+ >
177
+ Add
178
+ </button>
179
+
180
+ {/* Play (aligned under Search button) */}
181
+ <button
182
+ className={`btn bg-primary-600 hover:bg-primary-700 active:bg-primary-800 px-3 py-1.5 rounded-lg justify-center font-medium transition-all duration-200 shadow-md hover:shadow-glow ${ACTION_BTN_WIDTH}`}
183
+ onClick={() => playNow(r.url)}
184
+ title="Play now"
185
+ >
186
+ Play
187
+ </button>
188
+ </div>
189
+ ))}
190
+ </div>
191
+ </div>
192
+ )}
193
  </div>
194
  )
195
  }
lib/room.ts CHANGED
@@ -14,14 +14,16 @@ export const updateLastSync = (room: RoomState) => {
14
  return room
15
  }
16
 
17
- export const createNewUser = async (roomId: string, socketId: string) => {
18
  const room = await getRoom(roomId)
19
  if (room === null) {
20
  throw new Error("Creating user for non existing room:" + roomId)
21
  }
22
 
23
  const users = room.users
24
- let name = getRandomName()
 
 
25
  while (users.some((user) => user.name === name)) {
26
  name = getRandomName()
27
  }
@@ -54,7 +56,7 @@ export const createNewUser = async (roomId: string, socketId: string) => {
54
  export const createNewRoom = async (
55
  roomId: string,
56
  socketId: string,
57
- ownerName?: string,
58
  isPublic?: boolean
59
  ) => {
60
  // Use default image if available, otherwise use default video
@@ -67,7 +69,7 @@ export const createNewRoom = async (
67
  commandHistory: [],
68
  id: roomId,
69
  ownerId: socketId,
70
- ownerName: ownerName,
71
  isPublic: isPublic ?? false, // Default to private
72
  targetState: {
73
  playlist: {
 
14
  return room
15
  }
16
 
17
+ export const createNewUser = async (roomId: string, socketId: string, userName?: string) => {
18
  const room = await getRoom(roomId)
19
  if (room === null) {
20
  throw new Error("Creating user for non existing room:" + roomId)
21
  }
22
 
23
  const users = room.users
24
+ let name = userName && userName.trim() ? userName.trim() : getRandomName()
25
+
26
+ // Ensure unique name
27
  while (users.some((user) => user.name === name)) {
28
  name = getRandomName()
29
  }
 
56
  export const createNewRoom = async (
57
  roomId: string,
58
  socketId: string,
59
+ userName?: string,
60
  isPublic?: boolean
61
  ) => {
62
  // Use default image if available, otherwise use default video
 
69
  commandHistory: [],
70
  id: roomId,
71
  ownerId: socketId,
72
+ ownerName: userName,
73
  isPublic: isPublic ?? false, // Default to private
74
  targetState: {
75
  playlist: {
lib/socket.ts CHANGED
@@ -66,13 +66,13 @@ export function playItemFromPlaylist(
66
 
67
  export function createClientSocket(
68
  roomId: string,
69
- ownerName?: string,
70
  isPublic?: boolean
71
  ): TypedSocket {
72
  console.log("Trying to join room", roomId)
73
  const query: any = { roomId }
74
- if (ownerName) {
75
- query.ownerName = ownerName
76
  }
77
  if (isPublic !== undefined) {
78
  query.isPublic = isPublic.toString()
 
66
 
67
  export function createClientSocket(
68
  roomId: string,
69
+ userName?: string,
70
  isPublic?: boolean
71
  ): TypedSocket {
72
  console.log("Trying to join room", roomId)
73
  const query: any = { roomId }
74
+ if (userName) {
75
+ query.userName = userName
76
  }
77
  if (isPublic !== undefined) {
78
  query.isPublic = isPublic.toString()
pages/api/socketio.ts CHANGED
@@ -69,8 +69,8 @@ const ioHandler = (_: NextApiRequest, res: NextApiResponse) => {
69
  }
70
 
71
  const roomId = socket.handshake.query.roomId.toLowerCase()
72
- const ownerName = typeof socket.handshake.query.ownerName === "string"
73
- ? socket.handshake.query.ownerName
74
  : undefined
75
  const isPublic = socket.handshake.query.isPublic === "true"
76
 
@@ -83,15 +83,15 @@ const ioHandler = (_: NextApiRequest, res: NextApiResponse) => {
83
  }
84
 
85
  if (!(await roomExists(roomId))) {
86
- await createNewRoom(roomId, socket.id, ownerName, isPublic)
87
- log("created room", { ownerName, isPublic })
88
  }
89
 
90
  socket.join(roomId)
91
  await incUsers()
92
  log("joined")
93
 
94
- await createNewUser(roomId, socket.id)
95
 
96
  // Send initial chat history to the newly joined socket
97
  {
 
69
  }
70
 
71
  const roomId = socket.handshake.query.roomId.toLowerCase()
72
+ const userName = typeof socket.handshake.query.userName === "string"
73
+ ? socket.handshake.query.userName
74
  : undefined
75
  const isPublic = socket.handshake.query.isPublic === "true"
76
 
 
83
  }
84
 
85
  if (!(await roomExists(roomId))) {
86
+ await createNewRoom(roomId, socket.id, userName, isPublic)
87
+ log("created room", { userName, isPublic })
88
  }
89
 
90
  socket.join(roomId)
91
  await incUsers()
92
  log("joined")
93
 
94
+ await createNewUser(roomId, socket.id, userName)
95
 
96
  // Send initial chat history to the newly joined socket
97
  {