Arnav Singh commited on
Commit
d193f65
·
unverified ·
2 Parent(s): 97a853c90ee3c2

Merge pull request #1 from ArnavSingh76533/copilot/add-room-creation-ui-features

Browse files
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
@@ -1,8 +1,26 @@
1
  import { RoomState } from "./types"
2
 
3
- // In-memory stores
4
- const rooms = new Map<string, RoomState>()
5
- let userCount = 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
  export const getRoom = async (roomId: string): Promise<RoomState | null> => {
8
  return rooms.get(roomId) ?? null
@@ -31,21 +49,33 @@ export const countRooms = async (): Promise<number> => {
31
  }
32
 
33
  export const countUsers = async (): Promise<number> => {
34
- return userCount
35
  }
36
 
37
  export const incUsers = async (): Promise<number> => {
38
- userCount += 1
39
- return userCount
 
40
  }
41
 
42
  export const decUsers = async (): Promise<number> => {
43
- userCount = Math.max(0, userCount - 1)
44
- return userCount
 
45
  }
46
 
47
  export const wipeCache = async (): Promise<"OK"> => {
48
  rooms.clear()
49
- userCount = 0
50
  return "OK"
 
 
 
 
 
 
 
 
 
 
51
  }
 
1
  import { RoomState } from "./types"
2
 
3
+ // Use global object to ensure cache is shared across all API routes in development
4
+ // In Next.js dev mode, modules can be loaded multiple times, so we need to use global
5
+ const globalForCache = global as typeof globalThis & {
6
+ roomsCache?: Map<string, RoomState>
7
+ userCount?: number
8
+ }
9
+
10
+ // In-memory stores - use global to persist across hot reloads
11
+ const rooms = globalForCache.roomsCache || new Map<string, RoomState>()
12
+ if (!globalForCache.roomsCache) {
13
+ globalForCache.roomsCache = rooms
14
+ }
15
+
16
+ if (!globalForCache.userCount) {
17
+ globalForCache.userCount = 0
18
+ }
19
+
20
+ const getUserCount = () => globalForCache.userCount || 0
21
+ const setUserCount = (count: number) => {
22
+ globalForCache.userCount = count
23
+ }
24
 
25
  export const getRoom = async (roomId: string): Promise<RoomState | null> => {
26
  return rooms.get(roomId) ?? null
 
49
  }
50
 
51
  export const countUsers = async (): Promise<number> => {
52
+ return getUserCount()
53
  }
54
 
55
  export const incUsers = async (): Promise<number> => {
56
+ const newCount = getUserCount() + 1
57
+ setUserCount(newCount)
58
+ return newCount
59
  }
60
 
61
  export const decUsers = async (): Promise<number> => {
62
+ const newCount = Math.max(0, getUserCount() - 1)
63
+ setUserCount(newCount)
64
+ return newCount
65
  }
66
 
67
  export const wipeCache = async (): Promise<"OK"> => {
68
  rooms.clear()
69
+ setUserCount(0)
70
  return "OK"
71
+ }
72
+
73
+ export const getPublicRooms = async (): Promise<RoomState[]> => {
74
+ const publicRooms: RoomState[] = []
75
+ for (const [_, room] of rooms) {
76
+ if (room.isPublic) {
77
+ publicRooms.push(room)
78
+ }
79
+ }
80
+ return publicRooms
81
  }
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[]
package-lock.json CHANGED
@@ -1,5 +1,5 @@
1
  {
2
- "name": "streamer3",
3
  "lockfileVersion": 3,
4
  "requires": true,
5
  "packages": {
 
1
  {
2
+ "name": "Stream4",
3
  "lockfileVersion": 3,
4
  "requires": true,
5
  "packages": {
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,39 @@ 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 +105,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 +125,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 +158,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 +192,49 @@ 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 } = 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
+ <span className="text-xs text-dark-500">(optional for joining, required for creating)</span>
72
+ </label>
73
+ <InputText
74
+ value={userName}
75
+ placeholder={"Enter your display name"}
76
+ onChange={(value) => {
77
+ setUserName(value)
78
+ setNameError("")
79
+ }}
80
+ />
81
+ {nameError && (
82
+ <p className="text-red-500 text-xs mt-1">{nameError}</p>
83
+ )}
84
+ </div>
85
 
86
+ {/* Room ID Input */}
87
+ <div>
88
+ <label className="block text-sm font-medium text-dark-300 mb-1.5">
89
+ Room ID
90
+ </label>
91
+ <InputText
92
+ value={room}
93
+ placeholder={"Enter a room ID"}
94
+ onChange={(value) =>
95
+ setRoom(value.toLowerCase().replace(/[^a-z]/g, ""))
96
+ }
97
+ />
98
+ </div>
99
 
100
  <div className={"flex gap-3 justify-end"}>
101
  <Button
 
105
  "bg-accent-600 hover:bg-accent-700 active:bg-accent-800 shadow-lg hover:shadow-xl"
106
  }
107
  onClick={() => {
108
+ // Validate name
109
+ if (!userName.trim()) {
110
+ setNameError("Please enter your name before creating a room")
111
+ return
112
+ }
113
+
114
+ // Save user name to localStorage
115
+ if (typeof window !== "undefined") {
116
+ localStorage.setItem("userName", userName.trim())
117
+ }
118
+
119
  fetch("/api/generate")
120
  .then((r) => r.json())
121
  .then(async ({ roomId }) => {
 
125
  roomId.match(/^[a-z]{4,}$/)
126
  ) {
127
  console.log("Generated new roomId:", roomId)
128
+ // Store room metadata in sessionStorage
129
+ if (typeof window !== "undefined") {
130
+ sessionStorage.setItem(
131
+ `room_${roomId}_meta`,
132
+ JSON.stringify({ isPublic })
133
+ )
134
+ }
135
  await router.push("/room/" + roomId)
136
  } else {
137
  throw Error("Invalid roomId generated: " + roomId)
 
158
  Join room
159
  </Button>
160
  </div>
161
+
162
+ {/* Public/Private Toggle */}
163
+ <div className="flex items-center gap-3 p-3 bg-dark-800/50 rounded-lg border border-dark-700/30">
164
+ <label className="flex items-center gap-2 cursor-pointer flex-1">
165
+ <input
166
+ type="checkbox"
167
+ checked={isPublic}
168
+ onChange={(e) => setIsPublic(e.target.checked)}
169
+ className="w-4 h-4 rounded border-dark-600 text-primary-600 focus:ring-primary-500 focus:ring-offset-dark-900"
170
+ />
171
+ <span className="text-sm text-dark-300">
172
+ Make room public (visible in lobby)
173
+ </span>
174
+ </label>
175
+ </div>
176
 
177
  <div className={"mt-2 pt-4 border-t border-dark-700/50"}>
178
  <small className={"text-dark-400"}>
 
192
  </form>
193
  </div>
194
 
195
+ {/* Public Rooms List */}
196
+ {roomsData && roomsData.rooms && roomsData.rooms.length > 0 && (
197
+ <div className="max-w-4xl mx-auto px-4 pb-8">
198
+ <div className="bg-gradient-to-br from-dark-800 to-dark-900 rounded-xl shadow-2xl p-6 border border-dark-700/50">
199
+ <h2 className="text-xl font-bold text-dark-200 mb-4 flex items-center gap-2">
200
+ <span className="inline-block w-2 h-2 bg-primary-500 rounded-full"></span>
201
+ Public Rooms
202
+ </h2>
203
+ <div className="space-y-2">
204
+ {roomsData.rooms.map((room) => (
205
+ <div
206
+ key={room.id}
207
+ 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"
208
+ >
209
+ <div className="flex-1">
210
+ <div className="font-medium text-dark-200">
211
+ Room: <span className="text-primary-400">{room.id}</span>
212
+ </div>
213
+ <div className="text-sm text-dark-400 mt-1">
214
+ Owner: {room.ownerName} • {room.memberCount} {room.memberCount === 1 ? "member" : "members"}
215
+ </div>
216
+ </div>
217
+ <Button
218
+ tooltip="Join this room"
219
+ className="px-4 py-2 text-sm font-medium"
220
+ actionClasses="bg-primary-600 hover:bg-primary-700 active:bg-primary-800"
221
+ onClick={async () => {
222
+ // Save user name if provided
223
+ if (userName.trim() && typeof window !== "undefined") {
224
+ localStorage.setItem("userName", userName.trim())
225
+ }
226
+ await router.push("/room/" + room.id)
227
+ }}
228
+ >
229
+ Join
230
+ </Button>
231
+ </div>
232
+ ))}
233
+ </div>
234
+ </div>
235
+ </div>
236
+ )}
237
+
238
  <Tooltip
239
  style={{
240
  backgroundColor: "var(--dark-700)",