File size: 4,857 Bytes
068ccc0
cb63061
068ccc0
 
 
cb63061
068ccc0
 
cb63061
068ccc0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cb63061
068ccc0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cb63061
068ccc0
cb63061
068ccc0
 
 
 
 
 
 
 
 
 
 
cb63061
 
 
068ccc0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
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> &middot; Members: {r.members} &middot; {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>
  );
}