Update
This commit is contained in:
@@ -2,11 +2,17 @@ package state
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxActivityLogEntries = 400
|
||||||
|
adminLogBroadcastLimit = 200
|
||||||
|
)
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
rooms map[string]*Room
|
rooms map[string]*Room
|
||||||
@@ -111,8 +117,10 @@ func (m *Manager) CreateRoom(input CreateRoomInput) (CreateRoomResult, error) {
|
|||||||
Participants: map[string]*Participant{
|
Participants: map[string]*Participant{
|
||||||
creatorID: creator,
|
creatorID: creator,
|
||||||
},
|
},
|
||||||
|
ActivityLog: []ActivityLogEntry{},
|
||||||
subscribers: map[string]*subscriber{},
|
subscribers: map[string]*subscriber{},
|
||||||
}
|
}
|
||||||
|
m.appendActivityLogLocked(room, "%s created the room as admin.", creator.Username)
|
||||||
|
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
m.rooms[roomID] = room
|
m.rooms[roomID] = room
|
||||||
@@ -176,12 +184,16 @@ func (m *Manager) JoinRoom(roomID string, input JoinRoomInput) (JoinRoomResult,
|
|||||||
return JoinRoomResult{}, ErrParticipantNotFound
|
return JoinRoomResult{}, ErrParticipantNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wasConnected := existing.Connected
|
||||||
existing.Username = username
|
existing.Username = username
|
||||||
existing.Connected = true
|
existing.Connected = true
|
||||||
existing.UpdatedAt = now
|
existing.UpdatedAt = now
|
||||||
if isAdminByToken {
|
if isAdminByToken {
|
||||||
existing.IsAdmin = true
|
existing.IsAdmin = true
|
||||||
}
|
}
|
||||||
|
if !wasConnected {
|
||||||
|
m.appendActivityLogLocked(room, "%s joined as %s.", existing.Username, existing.Role)
|
||||||
|
}
|
||||||
|
|
||||||
room.UpdatedAt = now
|
room.UpdatedAt = now
|
||||||
if err := m.store.Save(room); err != nil {
|
if err := m.store.Save(room); err != nil {
|
||||||
@@ -225,6 +237,7 @@ func (m *Manager) JoinRoom(roomID string, input JoinRoomInput) (JoinRoomResult,
|
|||||||
}
|
}
|
||||||
|
|
||||||
room.Participants[participant.ID] = participant
|
room.Participants[participant.ID] = participant
|
||||||
|
m.appendActivityLogLocked(room, "%s joined as %s.", participant.Username, participant.Role)
|
||||||
room.UpdatedAt = now
|
room.UpdatedAt = now
|
||||||
|
|
||||||
if err := m.store.Save(room); err != nil {
|
if err := m.store.Save(room); err != nil {
|
||||||
@@ -254,9 +267,11 @@ func (m *Manager) LeaveRoom(roomID, participantID string) error {
|
|||||||
return ErrParticipantNotFound
|
return ErrParticipantNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
participant.Connected = false
|
if !participant.Connected {
|
||||||
participant.UpdatedAt = nowUTC()
|
return nil
|
||||||
room.UpdatedAt = nowUTC()
|
}
|
||||||
|
m.disconnectParticipantLocked(room, participant)
|
||||||
|
m.appendActivityLogLocked(room, "%s left the room.", participant.Username)
|
||||||
|
|
||||||
if err := m.store.Save(room); err != nil {
|
if err := m.store.Save(room); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -300,9 +315,11 @@ func (m *Manager) CastVote(roomID, participantID, card string) error {
|
|||||||
participant.VoteValue = normalizedCard
|
participant.VoteValue = normalizedCard
|
||||||
participant.UpdatedAt = nowUTC()
|
participant.UpdatedAt = nowUTC()
|
||||||
room.UpdatedAt = nowUTC()
|
room.UpdatedAt = nowUTC()
|
||||||
|
m.appendActivityLogLocked(room, "%s voted %s.", participant.Username, normalizedCard)
|
||||||
|
|
||||||
if room.Settings.RevealMode == RevealModeAutoAll && allActiveParticipantsVoted(room) {
|
if room.Settings.RevealMode == RevealModeAutoAll && allActiveParticipantsVoted(room) {
|
||||||
room.Round.Revealed = true
|
room.Round.Revealed = true
|
||||||
|
m.appendActivityLogLocked(room, "Votes auto-revealed after all active participants voted.")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.store.Save(room); err != nil {
|
if err := m.store.Save(room); err != nil {
|
||||||
@@ -332,6 +349,7 @@ func (m *Manager) RevealVotes(roomID, participantID string) error {
|
|||||||
|
|
||||||
room.Round.Revealed = true
|
room.Round.Revealed = true
|
||||||
room.UpdatedAt = nowUTC()
|
room.UpdatedAt = nowUTC()
|
||||||
|
m.appendActivityLogLocked(room, "%s revealed the votes.", participant.Username)
|
||||||
|
|
||||||
if err := m.store.Save(room); err != nil {
|
if err := m.store.Save(room); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -360,6 +378,7 @@ func (m *Manager) ResetVotes(roomID, participantID string) error {
|
|||||||
|
|
||||||
m.resetVotesLocked(room)
|
m.resetVotesLocked(room)
|
||||||
room.UpdatedAt = nowUTC()
|
room.UpdatedAt = nowUTC()
|
||||||
|
m.appendActivityLogLocked(room, "%s reset all votes.", participant.Username)
|
||||||
|
|
||||||
if err := m.store.Save(room); err != nil {
|
if err := m.store.Save(room); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -421,10 +440,11 @@ func (m *Manager) Subscribe(roomID, participantID string) (<-chan []byte, []byte
|
|||||||
delete(roomRef.subscribers, subscriberID)
|
delete(roomRef.subscribers, subscriberID)
|
||||||
|
|
||||||
if p, participantOK := roomRef.Participants[participantID]; participantOK {
|
if p, participantOK := roomRef.Participants[participantID]; participantOK {
|
||||||
p.Connected = false
|
if p.Connected {
|
||||||
p.UpdatedAt = nowUTC()
|
m.disconnectParticipantLocked(roomRef, p)
|
||||||
roomRef.UpdatedAt = nowUTC()
|
m.appendActivityLogLocked(roomRef, "%s disconnected.", p.Username)
|
||||||
_ = m.store.Save(roomRef)
|
_ = m.store.Save(roomRef)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
roomRef.mu.Unlock()
|
roomRef.mu.Unlock()
|
||||||
|
|
||||||
@@ -461,6 +481,7 @@ func (m *Manager) loadFromDisk() error {
|
|||||||
Settings: persisted.Settings,
|
Settings: persisted.Settings,
|
||||||
Round: persisted.Round,
|
Round: persisted.Round,
|
||||||
Participants: make(map[string]*Participant, len(persisted.Participants)),
|
Participants: make(map[string]*Participant, len(persisted.Participants)),
|
||||||
|
ActivityLog: append([]ActivityLogEntry(nil), persisted.ActivityLog...),
|
||||||
subscribers: map[string]*subscriber{},
|
subscribers: map[string]*subscriber{},
|
||||||
}
|
}
|
||||||
for _, participant := range persisted.Participants {
|
for _, participant := range persisted.Participants {
|
||||||
@@ -489,6 +510,7 @@ func (room *Room) toPersisted() persistedRoom {
|
|||||||
Settings: room.Settings,
|
Settings: room.Settings,
|
||||||
Round: room.Round,
|
Round: room.Round,
|
||||||
Participants: participants,
|
Participants: participants,
|
||||||
|
ActivityLog: append([]ActivityLogEntry(nil), room.ActivityLog...),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -572,6 +594,18 @@ func (m *Manager) marshalRoomState(room *Room, viewerParticipantID string) ([]by
|
|||||||
|
|
||||||
if viewer.IsAdmin {
|
if viewer.IsAdmin {
|
||||||
state.Links.AdminLink = "/room/" + room.ID + "?adminToken=" + room.AdminToken
|
state.Links.AdminLink = "/room/" + room.ID + "?adminToken=" + room.AdminToken
|
||||||
|
|
||||||
|
start := 0
|
||||||
|
if len(room.ActivityLog) > adminLogBroadcastLimit {
|
||||||
|
start = len(room.ActivityLog) - adminLogBroadcastLimit
|
||||||
|
}
|
||||||
|
state.AdminLogs = make([]PublicActivityLogEntry, 0, len(room.ActivityLog)-start)
|
||||||
|
for _, item := range room.ActivityLog[start:] {
|
||||||
|
state.AdminLogs = append(state.AdminLogs, PublicActivityLogEntry{
|
||||||
|
At: item.At,
|
||||||
|
Message: item.Message,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return json.Marshal(state)
|
return json.Marshal(state)
|
||||||
@@ -606,3 +640,20 @@ func (m *Manager) broadcastRoom(roomID string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) appendActivityLogLocked(room *Room, format string, args ...any) {
|
||||||
|
room.ActivityLog = append(room.ActivityLog, ActivityLogEntry{
|
||||||
|
At: nowUTC(),
|
||||||
|
Message: fmt.Sprintf(format, args...),
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(room.ActivityLog) > maxActivityLogEntries {
|
||||||
|
room.ActivityLog = append([]ActivityLogEntry(nil), room.ActivityLog[len(room.ActivityLog)-maxActivityLogEntries:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) disconnectParticipantLocked(room *Room, participant *Participant) {
|
||||||
|
participant.Connected = false
|
||||||
|
participant.UpdatedAt = nowUTC()
|
||||||
|
room.UpdatedAt = nowUTC()
|
||||||
|
}
|
||||||
|
|||||||
@@ -54,6 +54,11 @@ type RoundState struct {
|
|||||||
Revealed bool `json:"revealed"`
|
Revealed bool `json:"revealed"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ActivityLogEntry struct {
|
||||||
|
At time.Time `json:"at"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
type persistedRoom struct {
|
type persistedRoom struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
AdminToken string `json:"adminToken"`
|
AdminToken string `json:"adminToken"`
|
||||||
@@ -62,6 +67,7 @@ type persistedRoom struct {
|
|||||||
Settings RoomSettings `json:"settings"`
|
Settings RoomSettings `json:"settings"`
|
||||||
Round RoundState `json:"round"`
|
Round RoundState `json:"round"`
|
||||||
Participants []*Participant `json:"participants"`
|
Participants []*Participant `json:"participants"`
|
||||||
|
ActivityLog []ActivityLogEntry `json:"activityLog,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type subscriber struct {
|
type subscriber struct {
|
||||||
@@ -77,6 +83,7 @@ type Room struct {
|
|||||||
Settings RoomSettings
|
Settings RoomSettings
|
||||||
Round RoundState
|
Round RoundState
|
||||||
Participants map[string]*Participant
|
Participants map[string]*Participant
|
||||||
|
ActivityLog []ActivityLogEntry
|
||||||
|
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
subscribers map[string]*subscriber
|
subscribers map[string]*subscriber
|
||||||
@@ -133,6 +140,11 @@ type RoomLinks struct {
|
|||||||
AdminLink string `json:"adminLink,omitempty"`
|
AdminLink string `json:"adminLink,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PublicActivityLogEntry struct {
|
||||||
|
At time.Time `json:"at"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
type PublicRoomState struct {
|
type PublicRoomState struct {
|
||||||
RoomID string `json:"roomId"`
|
RoomID string `json:"roomId"`
|
||||||
RoomName string `json:"roomName"`
|
RoomName string `json:"roomName"`
|
||||||
@@ -148,4 +160,5 @@ type PublicRoomState struct {
|
|||||||
SelfParticipantID string `json:"selfParticipantId"`
|
SelfParticipantID string `json:"selfParticipantId"`
|
||||||
ViewerIsAdmin bool `json:"viewerIsAdmin"`
|
ViewerIsAdmin bool `json:"viewerIsAdmin"`
|
||||||
Links RoomLinks `json:"links"`
|
Links RoomLinks `json:"links"`
|
||||||
|
AdminLogs []PublicActivityLogEntry `json:"adminLogs,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,6 +109,7 @@
|
|||||||
<div id="admin-controls" class="admin-controls hidden">
|
<div id="admin-controls" class="admin-controls hidden">
|
||||||
<button type="button" id="reveal-btn" class="btn">Reveal</button>
|
<button type="button" id="reveal-btn" class="btn">Reveal</button>
|
||||||
<button type="button" id="reset-btn" class="btn">Reset</button>
|
<button type="button" id="reset-btn" class="btn">Reset</button>
|
||||||
|
<button type="button" id="terminal-btn" class="btn">Terminal</button>
|
||||||
</div>
|
</div>
|
||||||
<p id="room-status" class="status-line">Waiting for join...</p>
|
<p id="room-status" class="status-line">Waiting for join...</p>
|
||||||
</section>
|
</section>
|
||||||
@@ -148,6 +149,20 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<div id="terminal-modal-overlay" class="terminal-modal-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="terminal-title">
|
||||||
|
<section class="window terminal-window">
|
||||||
|
<div class="title-bar">
|
||||||
|
<span id="terminal-title">RoomTerminal.exe</span>
|
||||||
|
<div class="title-bar-controls">
|
||||||
|
<button type="button" id="terminal-close-btn">×</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="window-content terminal-window-content">
|
||||||
|
<div id="terminal-log-output" class="terminal-log-output" aria-live="polite"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="taskbar desktop-taskbar" aria-label="Desktop taskbar">
|
<footer class="taskbar desktop-taskbar" aria-label="Desktop taskbar">
|
||||||
|
|||||||
@@ -373,6 +373,8 @@
|
|||||||
.admin-controls {
|
.admin-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
gap: 0.35rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.join-window {
|
.join-window {
|
||||||
@@ -384,6 +386,30 @@
|
|||||||
width: min(27rem, 92vw);
|
width: min(27rem, 92vw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.terminal-modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 72;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-window {
|
||||||
|
width: min(46rem, 94vw);
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-window-content {
|
||||||
|
padding: 0.55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-log-output {
|
||||||
|
height: min(55vh, 30rem);
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0.5rem;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.skeleton-line,
|
.skeleton-line,
|
||||||
.skeleton-board,
|
.skeleton-board,
|
||||||
.skeleton-table,
|
.skeleton-table,
|
||||||
|
|||||||
@@ -172,3 +172,27 @@ input[type="number"]::-webkit-inner-spin-button {
|
|||||||
40% { transform: scale(0.91); }
|
40% { transform: scale(0.91); }
|
||||||
100% { transform: scale(1); }
|
100% { transform: scale(1); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.terminal-window .title-bar {
|
||||||
|
background: #0f0f0f;
|
||||||
|
color: #9dff9d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-window .title-bar-controls button {
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #9dff9d;
|
||||||
|
border-color: #2e2e2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-log-output {
|
||||||
|
background: #020a02;
|
||||||
|
color: #84ff84;
|
||||||
|
border: 1px solid #0f4f0f;
|
||||||
|
font-family: "Courier New", Courier, monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-log-line {
|
||||||
|
margin-bottom: 0.18rem;
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,9 +16,13 @@ const participantList = document.getElementById('participant-list');
|
|||||||
const adminControls = document.getElementById('admin-controls');
|
const adminControls = document.getElementById('admin-controls');
|
||||||
const revealBtn = document.getElementById('reveal-btn');
|
const revealBtn = document.getElementById('reveal-btn');
|
||||||
const resetBtn = document.getElementById('reset-btn');
|
const resetBtn = document.getElementById('reset-btn');
|
||||||
|
const terminalBtn = document.getElementById('terminal-btn');
|
||||||
const shareLinkInput = document.getElementById('share-link');
|
const shareLinkInput = document.getElementById('share-link');
|
||||||
const shareAdminToggle = document.getElementById('share-admin-toggle');
|
const shareAdminToggle = document.getElementById('share-admin-toggle');
|
||||||
const roomStatus = document.getElementById('room-status');
|
const roomStatus = document.getElementById('room-status');
|
||||||
|
const terminalModalOverlay = document.getElementById('terminal-modal-overlay');
|
||||||
|
const terminalCloseBtn = document.getElementById('terminal-close-btn');
|
||||||
|
const terminalLogOutput = document.getElementById('terminal-log-output');
|
||||||
|
|
||||||
const joinPanel = document.getElementById('join-panel');
|
const joinPanel = document.getElementById('join-panel');
|
||||||
const joinForm = document.getElementById('join-form');
|
const joinForm = document.getElementById('join-form');
|
||||||
@@ -31,6 +35,7 @@ let participantID = params.get('participantId') || '';
|
|||||||
let adminToken = params.get('adminToken') || '';
|
let adminToken = params.get('adminToken') || '';
|
||||||
let eventSource = null;
|
let eventSource = null;
|
||||||
let latestLinks = { participantLink: '', adminLink: '' };
|
let latestLinks = { participantLink: '', adminLink: '' };
|
||||||
|
let latestAdminLogs = [];
|
||||||
|
|
||||||
const savedUsername = localStorage.getItem(USERNAME_KEY) || '';
|
const savedUsername = localStorage.getItem(USERNAME_KEY) || '';
|
||||||
joinUsernameInput.value = savedUsername;
|
joinUsernameInput.value = savedUsername;
|
||||||
@@ -249,6 +254,42 @@ function renderCards(cards, participants, isRevealed) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatLogTime(raw) {
|
||||||
|
const parsed = new Date(raw);
|
||||||
|
if (Number.isNaN(parsed.getTime())) {
|
||||||
|
return raw || '-';
|
||||||
|
}
|
||||||
|
return parsed.toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTerminalLogs(logs) {
|
||||||
|
terminalLogOutput.innerHTML = '';
|
||||||
|
if (!Array.isArray(logs) || logs.length === 0) {
|
||||||
|
const emptyLine = document.createElement('div');
|
||||||
|
emptyLine.className = 'terminal-log-line';
|
||||||
|
emptyLine.textContent = '[system] no activity recorded yet';
|
||||||
|
terminalLogOutput.appendChild(emptyLine);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logs.forEach((item) => {
|
||||||
|
const line = document.createElement('div');
|
||||||
|
line.className = 'terminal-log-line';
|
||||||
|
line.textContent = `[${formatLogTime(item.at)}] ${item.message}`;
|
||||||
|
terminalLogOutput.appendChild(line);
|
||||||
|
});
|
||||||
|
terminalLogOutput.scrollTop = terminalLogOutput.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openTerminal() {
|
||||||
|
terminalModalOverlay.classList.remove('hidden');
|
||||||
|
renderTerminalLogs(latestAdminLogs);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeTerminal() {
|
||||||
|
terminalModalOverlay.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
function renderState(state) {
|
function renderState(state) {
|
||||||
roomTitle.textContent = `${state.roomName} (${state.roomId})`;
|
roomTitle.textContent = `${state.roomName} (${state.roomId})`;
|
||||||
revealModeLabel.textContent = `Reveal mode: ${state.revealMode}`;
|
revealModeLabel.textContent = `Reveal mode: ${state.revealMode}`;
|
||||||
@@ -263,8 +304,15 @@ function renderState(state) {
|
|||||||
|
|
||||||
if (state.viewerIsAdmin) {
|
if (state.viewerIsAdmin) {
|
||||||
adminControls.classList.remove('hidden');
|
adminControls.classList.remove('hidden');
|
||||||
|
terminalBtn.classList.remove('hidden');
|
||||||
} else {
|
} else {
|
||||||
adminControls.classList.add('hidden');
|
adminControls.classList.add('hidden');
|
||||||
|
terminalBtn.classList.add('hidden');
|
||||||
|
closeTerminal();
|
||||||
|
}
|
||||||
|
latestAdminLogs = Array.isArray(state.adminLogs) ? state.adminLogs : [];
|
||||||
|
if (state.viewerIsAdmin && !terminalModalOverlay.classList.contains('hidden')) {
|
||||||
|
renderTerminalLogs(latestAdminLogs);
|
||||||
}
|
}
|
||||||
|
|
||||||
const votedCount = state.participants.filter((p) => p.connected && p.role === 'participant' && p.hasVoted).length;
|
const votedCount = state.participants.filter((p) => p.connected && p.role === 'participant' && p.hasVoted).length;
|
||||||
@@ -343,7 +391,19 @@ async function adminAction(action) {
|
|||||||
|
|
||||||
revealBtn.addEventListener('click', () => adminAction('reveal'));
|
revealBtn.addEventListener('click', () => adminAction('reveal'));
|
||||||
resetBtn.addEventListener('click', () => adminAction('reset'));
|
resetBtn.addEventListener('click', () => adminAction('reset'));
|
||||||
|
terminalBtn.addEventListener('click', openTerminal);
|
||||||
|
terminalCloseBtn.addEventListener('click', closeTerminal);
|
||||||
|
terminalModalOverlay.addEventListener('click', (event) => {
|
||||||
|
if (event.target === terminalModalOverlay) {
|
||||||
|
closeTerminal();
|
||||||
|
}
|
||||||
|
});
|
||||||
shareAdminToggle.addEventListener('change', updateShareLink);
|
shareAdminToggle.addEventListener('change', updateShareLink);
|
||||||
|
window.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
closeTerminal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
joinForm.addEventListener('submit', async (event) => {
|
joinForm.addEventListener('submit', async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|||||||
Reference in New Issue
Block a user