Spaces:
Sleeping
Sleeping
| import React, { useEffect, useMemo, useState } from 'react'; | |
| import Room from './Room.jsx'; | |
| import { ToastProvider, useToasts } from './Toasts.jsx'; | |
| import { log } from './logger.js'; | |
| import { prettyError } from './utils.js'; | |
| function Lobby({ onEnter }) { | |
| const { push } = useToasts(); | |
| const [name, setName] = useState(''); | |
| const [rooms, setRooms] = useState([]); | |
| const [loading, setLoading] = useState(false); | |
| const [creating, setCreating] = useState(false); | |
| const [newRoomName, setNewRoomName] = useState(''); | |
| const [newRoomId, setNewRoomId] = useState(''); | |
| const fetchRooms = async () => { | |
| try { | |
| setLoading(true); | |
| const res = await fetch('/api/rooms'); | |
| const json = await res.json(); | |
| setRooms(json.rooms || []); | |
| setLoading(false); | |
| } catch (e) { | |
| setLoading(false); | |
| log.error(e); | |
| push(`Failed to fetch rooms: ${prettyError(e)}`, 'bad', 4000); | |
| } | |
| }; | |
| useEffect(() => { | |
| fetchRooms(); | |
| const t = setInterval(fetchRooms, 5000); | |
| return () => clearInterval(t); | |
| }, []); | |
| const canEnter = useMemo(() => name.trim().length >= 2, [name]); | |
| return ( | |
| <div className="container"> | |
| <div className="panel" style={{ marginBottom: 16 }}> | |
| <div className="row"> | |
| <div className="col"> | |
| <div className="section-title">Your name</div> | |
| <input className="input" value={name} onChange={e => setName(e.target.value)} placeholder="DJ Neo" /> | |
| <div style={{ marginTop: 8, color: 'var(--muted)', fontSize: 13 }}> | |
| Your name appears in the party member list. | |
| </div> | |
| </div> | |
| <div className="col"> | |
| <div className="section-title">Create a new party</div> | |
| <div className="row" style={{ gap: 8 }}> | |
| <div className="col"> | |
| <input className="input" placeholder="Party name" value={newRoomName} onChange={e => setNewRoomName(e.target.value)} /> | |
| </div> | |
| <div className="col"> | |
| <input className="input" placeholder="Party ID (letters/numbers)" value={newRoomId} onChange={e => setNewRoomId(e.target.value.replace(/\s+/g, '-'))} /> | |
| </div> | |
| </div> | |
| <div style={{ marginTop: 10, display: 'flex', gap: 10 }}> | |
| <button | |
| className="btn primary" | |
| onClick={() => { | |
| if (!canEnter) return push('Please enter your name', 'warn'); | |
| if (!newRoomId.trim()) return push('Please provide a Party ID', 'warn'); | |
| onEnter({ roomId: newRoomId.trim(), name: name.trim(), asHost: true, roomName: newRoomName.trim() || newRoomId.trim() }); | |
| }} | |
| >Create & enter as host</button> | |
| <button className="btn" onClick={fetchRooms} disabled={loading}>{loading ? 'Refreshing...' : 'Refresh rooms'}</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="panel"> | |
| <div className="section-title">Active parties</div> | |
| {rooms.length === 0 ? ( | |
| <div style={{ color: 'var(--muted)' }}>No active parties yet. Create one above.</div> | |
| ) : ( | |
| <div className="room-grid"> | |
| {rooms.map(r => ( | |
| <div key={r.id} className="room-card"> | |
| <div style={{ fontWeight: 700 }}>{r.name}</div> | |
| <div className="meta" style={{ marginTop: 4 }}> | |
| ID: <span className="kbd">{r.id}</span> · Members: {r.members} · {r.isPlaying ? 'Playing' : 'Paused'} | |
| </div> | |
| <div style={{ marginTop: 10, display: 'flex', gap: 8 }}> | |
| <button | |
| className="btn good" | |
| onClick={() => { | |
| if (!canEnter) return push('Please enter your name', 'warn'); | |
| onEnter({ roomId: r.id, name: name.trim(), asHost: false }); | |
| }} | |
| >Join</button> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function Shell({ children }) { | |
| return ( | |
| <div className="app-shell"> | |
| <div className="top-glow"></div> | |
| <div className="header"> | |
| <div className="container"> | |
| <div className="brand"> | |
| <div className="logo"></div> | |
| <div>Neon Party</div> | |
| </div> | |
| </div> | |
| </div> | |
| {children} | |
| </div> | |
| ); | |
| } | |
| export default function App() { | |
| const [session, setSession] = useState(null); | |
| if (session) { | |
| return ( | |
| <ToastProvider> | |
| <Shell> | |
| <Room {...session} /> | |
| </Shell> | |
| </ToastProvider> | |
| ); | |
| } | |
| return ( | |
| <ToastProvider> | |
| <Shell> | |
| <Lobby onEnter={setSession} /> | |
| </Shell> | |
| </ToastProvider> | |
| ); | |
| } | |