copilot-swe-agent[bot] ArnavSingh76533 commited on
Commit
3394d66
·
1 Parent(s): c6742af

Add name input, public/private rooms, and lobby listing features

Browse files

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

components/Room.tsx CHANGED
@@ -35,7 +35,20 @@ const Room: FC<Props> = ({ id }) => {
35
  if (socket !== null) {
36
  setConnected(socket.connected)
37
  } else {
38
- const newSocket = createClientSocket(id)
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  newSocket.on("connect", () => {
40
  setConnected(true)
41
  })
 
35
  if (socket !== null) {
36
  setConnected(socket.connected)
37
  } else {
38
+ // Get user name from localStorage
39
+ const userName = typeof window !== "undefined"
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)
46
+ ? JSON.parse(sessionStorage.getItem(roomKey) || "{}")
47
+ : {}
48
+
49
+ const isPublic = roomMeta.isPublic
50
+
51
+ const newSocket = createClientSocket(id, userName, isPublic)
52
  newSocket.on("connect", () => {
53
  setConnected(true)
54
  })
lib/cache.ts CHANGED
@@ -48,4 +48,14 @@ export const wipeCache = async (): Promise<"OK"> => {
48
  rooms.clear()
49
  userCount = 0
50
  return "OK"
 
 
 
 
 
 
 
 
 
 
51
  }
 
48
  rooms.clear()
49
  userCount = 0
50
  return "OK"
51
+ }
52
+
53
+ export const getPublicRooms = async (): Promise<RoomState[]> => {
54
+ const publicRooms: RoomState[] = []
55
+ for (const [_, room] of rooms) {
56
+ if (room.isPublic) {
57
+ publicRooms.push(room)
58
+ }
59
+ }
60
+ return publicRooms
61
  }
lib/room.ts CHANGED
@@ -51,7 +51,12 @@ export const createNewUser = async (roomId: string, socketId: string) => {
51
  await setRoom(roomId, room)
52
  }
53
 
54
- export const createNewRoom = async (roomId: string, socketId: string) => {
 
 
 
 
 
55
  // Use default image if available, otherwise use default video
56
  const defaultImg = getDefaultImg()
57
  const defaultMedia = defaultImg || getDefaultSrc()
@@ -62,6 +67,8 @@ export const createNewRoom = async (roomId: string, socketId: string) => {
62
  commandHistory: [],
63
  id: roomId,
64
  ownerId: socketId,
 
 
65
  targetState: {
66
  playlist: {
67
  items: [
 
51
  await setRoom(roomId, room)
52
  }
53
 
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
61
  const defaultImg = getDefaultImg()
62
  const defaultMedia = defaultImg || getDefaultSrc()
 
67
  commandHistory: [],
68
  id: roomId,
69
  ownerId: socketId,
70
+ ownerName: ownerName,
71
+ isPublic: isPublic ?? false, // Default to private
72
  targetState: {
73
  playlist: {
74
  items: [
lib/socket.ts CHANGED
@@ -64,10 +64,22 @@ export function playItemFromPlaylist(
64
  socket.emit("playItemFromPlaylist", index)
65
  }
66
 
67
- export function createClientSocket(roomId: string): TypedSocket {
 
 
 
 
68
  console.log("Trying to join room", roomId)
 
 
 
 
 
 
 
 
69
  const socket: TypedSocket = io({
70
- query: { roomId },
71
  transports: ["websocket"],
72
  path: "/api/socketio",
73
  })
 
64
  socket.emit("playItemFromPlaylist", index)
65
  }
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()
79
+ }
80
+
81
  const socket: TypedSocket = io({
82
+ query,
83
  transports: ["websocket"],
84
  path: "/api/socketio",
85
  })
lib/types.ts CHANGED
@@ -77,6 +77,8 @@ export interface RoomState {
77
  serverTime: number
78
  id: string
79
  ownerId: string
 
 
80
  users: UserState[]
81
  targetState: TargetState
82
  commandHistory: CommandLog[]
 
77
  serverTime: number
78
  id: string
79
  ownerId: string
80
+ ownerName?: string // display name of the room owner
81
+ isPublic?: boolean // whether the room is public (shown in lobby) or private
82
  users: UserState[]
83
  targetState: TargetState
84
  commandHistory: CommandLog[]
pages/api/rooms.ts ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getPublicRooms } from "../../lib/cache"
2
+ import { NextApiRequest, NextApiResponse } from "next"
3
+
4
+ export default async function rooms(_: NextApiRequest, res: NextApiResponse) {
5
+ const publicRooms = await getPublicRooms()
6
+
7
+ // Return simplified room info for the lobby
8
+ const roomList = publicRooms.map(room => ({
9
+ id: room.id,
10
+ ownerName: room.ownerName || "Unknown",
11
+ memberCount: room.users.length,
12
+ }))
13
+
14
+ res.json({ rooms: roomList })
15
+ }
pages/api/socketio.ts CHANGED
@@ -69,6 +69,11 @@ const ioHandler = (_: NextApiRequest, res: NextApiResponse) => {
69
  }
70
 
71
  const roomId = socket.handshake.query.roomId.toLowerCase()
 
 
 
 
 
72
  const log = (...props: any[]) => {
73
  console.log(
74
  "[" + new Date().toUTCString() + "][room " + roomId + "]",
@@ -78,8 +83,8 @@ const ioHandler = (_: NextApiRequest, res: NextApiResponse) => {
78
  }
79
 
80
  if (!(await roomExists(roomId))) {
81
- await createNewRoom(roomId, socket.id)
82
- log("created room")
83
  }
84
 
85
  socket.join(roomId)
 
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
+
77
  const log = (...props: any[]) => {
78
  console.log(
79
  "[" + new Date().toUTCString() + "][room " + roomId + "]",
 
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)
pages/index.tsx CHANGED
@@ -1,17 +1,42 @@
1
  import Layout from "../components/Layout"
2
- import { useState } from "react"
3
  import InputText from "../components/input/InputText"
4
  import Button from "../components/action/Button"
5
  import { useRouter } from "next/router"
6
  import { Tooltip } from "react-tooltip"
7
  import useSWR from "swr"
8
 
 
 
 
 
 
 
9
  export default function Index() {
10
  const router = useRouter()
11
  const { data } = useSWR("/api/stats", (url) =>
12
  fetch(url).then((r) => r.json())
13
  )
 
 
 
 
 
 
14
  const [room, setRoom] = useState("")
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  return (
17
  <Layout meta={{ robots: "index, archive, follow" }} showNavbar={false}>
@@ -24,6 +49,10 @@ export default function Index() {
24
  e.preventDefault()
25
 
26
  if (room.length >= 4) {
 
 
 
 
27
  await router.push("/room/" + room)
28
  }
29
  }}
@@ -34,14 +63,38 @@ export default function Index() {
34
  </h1>
35
  <p className="text-dark-400 text-sm">Join or create a room to watch together</p>
36
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
- <InputText
39
- value={room}
40
- placeholder={"Enter a room ID"}
41
- onChange={(value) =>
42
- setRoom(value.toLowerCase().replace(/[^a-z]/g, ""))
43
- }
44
- />
 
 
 
 
 
 
45
 
46
  <div className={"flex gap-3 justify-end"}>
47
  <Button
@@ -51,6 +104,17 @@ export default function Index() {
51
  "bg-accent-600 hover:bg-accent-700 active:bg-accent-800 shadow-lg hover:shadow-xl"
52
  }
53
  onClick={() => {
 
 
 
 
 
 
 
 
 
 
 
54
  fetch("/api/generate")
55
  .then((r) => r.json())
56
  .then(async ({ roomId }) => {
@@ -60,6 +124,13 @@ export default function Index() {
60
  roomId.match(/^[a-z]{4,}$/)
61
  ) {
62
  console.log("Generated new roomId:", roomId)
 
 
 
 
 
 
 
63
  await router.push("/room/" + roomId)
64
  } else {
65
  throw Error("Invalid roomId generated: " + roomId)
@@ -86,6 +157,21 @@ export default function Index() {
86
  Join room
87
  </Button>
88
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
  <div className={"mt-2 pt-4 border-t border-dark-700/50"}>
91
  <small className={"text-dark-400"}>
@@ -105,6 +191,48 @@ export default function Index() {
105
  </form>
106
  </div>
107
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  <Tooltip
109
  style={{
110
  backgroundColor: "var(--dark-700)",
 
1
  import Layout from "../components/Layout"
2
+ import { useState, useEffect } from "react"
3
  import InputText from "../components/input/InputText"
4
  import Button from "../components/action/Button"
5
  import { useRouter } from "next/router"
6
  import { Tooltip } from "react-tooltip"
7
  import useSWR from "swr"
8
 
9
+ interface PublicRoom {
10
+ id: string
11
+ ownerName: string
12
+ memberCount: number
13
+ }
14
+
15
  export default function Index() {
16
  const router = useRouter()
17
  const { data } = useSWR("/api/stats", (url) =>
18
  fetch(url).then((r) => r.json())
19
  )
20
+ const { data: roomsData, mutate: refreshRooms } = useSWR<{ rooms: PublicRoom[] }>(
21
+ "/api/rooms",
22
+ (url) => fetch(url).then((r) => r.json()),
23
+ { refreshInterval: 5000 } // Auto-refresh every 5 seconds
24
+ )
25
+
26
  const [room, setRoom] = useState("")
27
+ const [userName, setUserName] = useState("")
28
+ const [isPublic, setIsPublic] = useState(false)
29
+ const [nameError, setNameError] = useState("")
30
+
31
+ // Load saved user name from localStorage on mount
32
+ useEffect(() => {
33
+ if (typeof window !== "undefined") {
34
+ const savedName = localStorage.getItem("userName")
35
+ if (savedName) {
36
+ setUserName(savedName)
37
+ }
38
+ }
39
+ }, [])
40
 
41
  return (
42
  <Layout meta={{ robots: "index, archive, follow" }} showNavbar={false}>
 
49
  e.preventDefault()
50
 
51
  if (room.length >= 4) {
52
+ // Save user name to localStorage
53
+ if (userName.trim() && typeof window !== "undefined") {
54
+ localStorage.setItem("userName", userName.trim())
55
+ }
56
  await router.push("/room/" + room)
57
  }
58
  }}
 
63
  </h1>
64
  <p className="text-dark-400 text-sm">Join or create a room to watch together</p>
65
  </div>
66
+
67
+ {/* User Name Input */}
68
+ <div>
69
+ <label className="block text-sm font-medium text-dark-300 mb-1.5">
70
+ Your Name
71
+ </label>
72
+ <InputText
73
+ value={userName}
74
+ placeholder={"Enter your display name"}
75
+ onChange={(value) => {
76
+ setUserName(value)
77
+ setNameError("")
78
+ }}
79
+ />
80
+ {nameError && (
81
+ <p className="text-red-500 text-xs mt-1">{nameError}</p>
82
+ )}
83
+ </div>
84
 
85
+ {/* Room ID Input */}
86
+ <div>
87
+ <label className="block text-sm font-medium text-dark-300 mb-1.5">
88
+ Room ID
89
+ </label>
90
+ <InputText
91
+ value={room}
92
+ placeholder={"Enter a room ID"}
93
+ onChange={(value) =>
94
+ setRoom(value.toLowerCase().replace(/[^a-z]/g, ""))
95
+ }
96
+ />
97
+ </div>
98
 
99
  <div className={"flex gap-3 justify-end"}>
100
  <Button
 
104
  "bg-accent-600 hover:bg-accent-700 active:bg-accent-800 shadow-lg hover:shadow-xl"
105
  }
106
  onClick={() => {
107
+ // Validate name
108
+ if (!userName.trim()) {
109
+ setNameError("Please enter your name before creating a room")
110
+ return
111
+ }
112
+
113
+ // Save user name to localStorage
114
+ if (typeof window !== "undefined") {
115
+ localStorage.setItem("userName", userName.trim())
116
+ }
117
+
118
  fetch("/api/generate")
119
  .then((r) => r.json())
120
  .then(async ({ roomId }) => {
 
124
  roomId.match(/^[a-z]{4,}$/)
125
  ) {
126
  console.log("Generated new roomId:", roomId)
127
+ // Store room metadata in sessionStorage
128
+ if (typeof window !== "undefined") {
129
+ sessionStorage.setItem(
130
+ `room_${roomId}_meta`,
131
+ JSON.stringify({ isPublic })
132
+ )
133
+ }
134
  await router.push("/room/" + roomId)
135
  } else {
136
  throw Error("Invalid roomId generated: " + roomId)
 
157
  Join room
158
  </Button>
159
  </div>
160
+
161
+ {/* Public/Private Toggle */}
162
+ <div className="flex items-center gap-3 p-3 bg-dark-800/50 rounded-lg border border-dark-700/30">
163
+ <label className="flex items-center gap-2 cursor-pointer flex-1">
164
+ <input
165
+ type="checkbox"
166
+ checked={isPublic}
167
+ onChange={(e) => setIsPublic(e.target.checked)}
168
+ className="w-4 h-4 rounded border-dark-600 text-primary-600 focus:ring-primary-500 focus:ring-offset-dark-900"
169
+ />
170
+ <span className="text-sm text-dark-300">
171
+ Make room public (visible in lobby)
172
+ </span>
173
+ </label>
174
+ </div>
175
 
176
  <div className={"mt-2 pt-4 border-t border-dark-700/50"}>
177
  <small className={"text-dark-400"}>
 
191
  </form>
192
  </div>
193
 
194
+ {/* Public Rooms List */}
195
+ {roomsData && roomsData.rooms && roomsData.rooms.length > 0 && (
196
+ <div className="max-w-4xl mx-auto px-4 pb-8">
197
+ <div className="bg-gradient-to-br from-dark-800 to-dark-900 rounded-xl shadow-2xl p-6 border border-dark-700/50">
198
+ <h2 className="text-xl font-bold text-dark-200 mb-4 flex items-center gap-2">
199
+ <span className="inline-block w-2 h-2 bg-primary-500 rounded-full"></span>
200
+ Public Rooms
201
+ </h2>
202
+ <div className="space-y-2">
203
+ {roomsData.rooms.map((room) => (
204
+ <div
205
+ key={room.id}
206
+ className="flex items-center justify-between p-4 bg-dark-800/50 rounded-lg border border-dark-700/30 hover:border-primary-500/50 transition-colors"
207
+ >
208
+ <div className="flex-1">
209
+ <div className="font-medium text-dark-200">
210
+ Room: <span className="text-primary-400">{room.id}</span>
211
+ </div>
212
+ <div className="text-sm text-dark-400 mt-1">
213
+ Owner: {room.ownerName} • {room.memberCount} {room.memberCount === 1 ? "member" : "members"}
214
+ </div>
215
+ </div>
216
+ <Button
217
+ className="px-4 py-2 text-sm font-medium"
218
+ actionClasses="bg-primary-600 hover:bg-primary-700 active:bg-primary-800"
219
+ onClick={async () => {
220
+ // Save user name if provided
221
+ if (userName.trim() && typeof window !== "undefined") {
222
+ localStorage.setItem("userName", userName.trim())
223
+ }
224
+ await router.push("/room/" + room.id)
225
+ }}
226
+ >
227
+ Join
228
+ </Button>
229
+ </div>
230
+ ))}
231
+ </div>
232
+ </div>
233
+ </div>
234
+ )}
235
+
236
  <Tooltip
237
  style={{
238
  backgroundColor: "var(--dark-700)",