diff --git a/webapp/public/main.js b/webapp/public/main.js
index 7bb0515..483f684 100644
--- a/webapp/public/main.js
+++ b/webapp/public/main.js
@@ -1,12 +1,16 @@
const statusEl = document.querySelector('#status');
const roomsEl = document.querySelector('#rooms');
-const chatEl = document.querySelector('#chat');
+const lobbyFeedEl = document.querySelector('#chat');
const sessionViewEl = document.querySelector('#session');
const createRoomResultEl = document.querySelector('#create-room-result');
const joinResultEl = document.querySelector('#join-result');
const difficultyCardsEl = document.querySelector('#difficulty-cards');
const legendEl = document.querySelector('#legend');
const startResultEl = document.querySelector('#start-result');
+const statRoomsEl = document.querySelector('#stat-rooms');
+const statPlayersEl = document.querySelector('#stat-players');
+const statWaitingEl = document.querySelector('#stat-waiting');
+const onlineUsersEl = document.querySelector('#online-users');
let difficultyDefinitions = [];
@@ -113,25 +117,88 @@ const renderRooms = (rooms = []) => {
});
};
-const renderChat = (messages = []) => {
- chatEl.innerHTML = '';
- messages
- .slice()
- .reverse()
- .forEach((message) => {
- const container = document.createElement('div');
- container.className = 'message';
- const meta = document.createElement('div');
- meta.className = 'meta';
- meta.textContent = `${message.author} • ${new Date(message.timestamp).toLocaleTimeString()}`;
+const renderLobbyFeed = (messages = [], rooms = []) => {
+ lobbyFeedEl.innerHTML = '';
+ const feedItems = [
+ ...messages.map((message) => ({ type: 'chat', timestamp: message.timestamp, payload: message })),
+ ...rooms.map((room) => ({ type: 'room', timestamp: room.createdAt, payload: room })),
+ ].sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
+
+ if (!feedItems.length) {
+ lobbyFeedEl.textContent = 'Lobby activity will appear here once people start chatting or hosting.';
+ return;
+ }
+
+ feedItems.forEach((entry) => {
+ const container = document.createElement('div');
+ container.className = `feed-item ${entry.type}`;
+
+ const meta = document.createElement('div');
+ meta.className = 'feed-meta';
+ const time = new Date(entry.timestamp).toLocaleTimeString();
+
+ if (entry.type === 'chat') {
+ const message = entry.payload;
+ const author = document.createElement('span');
+ author.className = 'feed-tag';
+ author.textContent = message.author;
+ meta.append(author, document.createTextNode(time));
+
if (message.sessionSummary) {
- meta.textContent += ` • ${message.sessionSummary.ownerName}'s lobby`;
+ const badge = document.createElement('span');
+ badge.className = 'feed-pill';
+ badge.textContent = `${message.sessionSummary.ownerName}'s lobby`;
+ meta.appendChild(badge);
}
+
const body = document.createElement('div');
+ body.className = 'feed-body';
body.textContent = message.body;
container.append(meta, body);
- chatEl.appendChild(container);
- });
+ }
+
+ if (entry.type === 'room') {
+ const room = entry.payload;
+ const label = document.createElement('span');
+ label.className = 'feed-tag';
+ label.textContent = `${room.ownerName} started a lobby`;
+ const badge = document.createElement('span');
+ badge.className = 'feed-pill';
+ badge.textContent = `${formatDifficulty(room.difficulty)} • ${room.playerCount}/${room.maxPlayers}`;
+ meta.append(label, document.createTextNode(time), badge);
+
+ const body = document.createElement('div');
+ body.className = 'feed-body';
+ body.textContent = room.allowJoinMidgame ? 'Mid-dungeon joins allowed' : 'Locks after the run begins';
+
+ const actions = document.createElement('div');
+ actions.className = 'feed-actions';
+ const copyButton = document.createElement('button');
+ copyButton.className = 'pill-button ghost';
+ copyButton.textContent = 'Copy Session ID';
+ copyButton.addEventListener('click', async () => {
+ await navigator.clipboard.writeText(room.sessionId);
+ document.querySelector('#join-session-id').value = room.sessionId;
+ document.querySelector('#session-id').value = room.sessionId;
+ alert(`Session ID copied: ${room.sessionId}`);
+ });
+
+ const joinButton = document.createElement('button');
+ joinButton.className = 'pill-button';
+ joinButton.textContent = 'Join lobby';
+ joinButton.addEventListener('click', () => {
+ document.querySelector('#join-session-id').value = room.sessionId;
+ document.querySelector('#session-id').value = room.sessionId;
+ document.querySelector('#chat-session').value = room.sessionId;
+ window.scrollTo({ top: document.querySelector('#join-form').offsetTop - 20, behavior: 'smooth' });
+ });
+
+ actions.append(copyButton, joinButton);
+ container.append(meta, body, actions);
+ }
+
+ lobbyFeedEl.appendChild(container);
+ });
};
const renderSession = (state) => {
@@ -208,12 +275,55 @@ const renderSession = (state) => {
sessionViewEl.appendChild(wrapper);
};
+const renderStats = (rooms = []) => {
+ const totalRooms = rooms.length;
+ const filledSeats = rooms.reduce((sum, room) => sum + (room.playerCount || 0), 0);
+ const maxSeats = rooms.reduce((sum, room) => sum + (room.maxPlayers || 0), 0);
+ const waitingRooms = rooms.filter((room) => room.status === 'waiting').length;
+
+ if (statRoomsEl) {
+ statRoomsEl.querySelector('strong').textContent = totalRooms;
+ statRoomsEl.querySelector('span').textContent = 'Active sessions';
+ }
+
+ if (statPlayersEl) {
+ statPlayersEl.querySelector('strong').textContent = maxSeats ? `${filledSeats}/${maxSeats}` : '—';
+ statPlayersEl.querySelector('span').textContent = 'Adventurers seated';
+ }
+
+ if (statWaitingEl) {
+ statWaitingEl.querySelector('strong').textContent = waitingRooms;
+ statWaitingEl.querySelector('span').textContent = 'Waiting rooms';
+ }
+};
+
+const renderOnlineUsers = (rooms = []) => {
+ if (!onlineUsersEl) return;
+ onlineUsersEl.innerHTML = '';
+
+ const hosts = rooms.map((room) => room.ownerName).filter(Boolean);
+ const uniqueHosts = Array.from(new Set(hosts));
+
+ if (!uniqueHosts.length) {
+ onlineUsersEl.textContent = 'Waiting for hosts to appear.';
+ return;
+ }
+
+ uniqueHosts.forEach((host) => {
+ const item = document.createElement('div');
+ item.textContent = host;
+ onlineUsersEl.appendChild(item);
+ });
+};
+
const refreshLobby = async () => {
try {
statusEl.textContent = 'Loading lobby...';
const snapshot = await api('/lobby');
renderRooms(snapshot.rooms);
- renderChat(snapshot.messages);
+ renderLobbyFeed(snapshot.messages, snapshot.rooms);
+ renderStats(snapshot.rooms);
+ renderOnlineUsers(snapshot.rooms);
statusEl.textContent = `Lobby loaded at ${new Date().toLocaleTimeString()}`;
} catch (error) {
statusEl.textContent = error.message;
@@ -369,6 +479,15 @@ const refreshSession = async (sessionId) => {
const refreshButton = document.querySelector('#refresh-lobby');
refreshButton?.addEventListener('click', () => refreshLobby());
+document.querySelectorAll('[data-scroll]').forEach((button) => {
+ button.addEventListener('click', (event) => {
+ const target = document.querySelector(event.currentTarget.dataset.scroll);
+ if (target) {
+ target.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ }
+ });
+});
+
loadDifficulties().then(() => refreshLobby());
renderLegend();
renderSession(null);
diff --git a/webapp/public/styles.css b/webapp/public/styles.css
index a147b8f..d191c51 100644
--- a/webapp/public/styles.css
+++ b/webapp/public/styles.css
@@ -1,38 +1,161 @@
:root {
- color-scheme: light dark;
- font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
- line-height: 1.4;
- padding: 0 1rem;
+ color-scheme: light;
+ font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+ line-height: 1.5;
+ background: radial-gradient(circle at 10% 20%, #2b1b56 0, transparent 25%),
+ radial-gradient(circle at 90% 0%, #1c4aa8 0, transparent 22%),
+ radial-gradient(circle at 50% 90%, #8b3cf5 0, transparent 35%),
+ linear-gradient(180deg, #0b0b1f 0%, #0e0c22 40%, #0a0a14 100%);
+ color: #f4f5ff;
+}
+
+* {
+ box-sizing: border-box;
}
body {
+ margin: 0;
+ min-height: 100vh;
+ background: transparent;
+ padding: 2rem;
+}
+
+.page-shell {
max-width: 1200px;
margin: 0 auto 3rem;
}
-header {
- text-align: center;
- padding: 1rem 0 0.5rem;
+.hero {
+ display: flex;
+ gap: 1.5rem;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1.75rem;
+ border-radius: 20px;
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.02));
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ box-shadow: 0 15px 60px rgba(0, 0, 0, 0.45), 0 0 25px rgba(139, 60, 245, 0.25);
+}
+
+.hero-copy h1 {
+ margin: 0 0 0.25rem;
+ font-size: 2.1rem;
+ letter-spacing: -0.01em;
+}
+
+.hero-actions {
+ display: flex;
+ gap: 0.5rem;
+ flex-wrap: wrap;
+ margin-top: 0.5rem;
+}
+
+.eyebrow {
+ text-transform: uppercase;
+ font-size: 0.75rem;
+ letter-spacing: 0.12em;
+ opacity: 0.75;
+ margin: 0 0 0.15rem;
+}
+
+.lede {
+ margin: 0;
+ color: rgba(244, 245, 255, 0.85);
+}
+
+.status-card {
+ display: flex;
+ gap: 0.75rem;
+ align-items: center;
+ padding: 1rem 1.25rem;
+ border-radius: 16px;
+ background: linear-gradient(135deg, rgba(87, 255, 224, 0.12), rgba(255, 255, 255, 0.04));
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ box-shadow: 0 12px 45px rgba(0, 0, 0, 0.35), 0 0 18px rgba(100, 223, 223, 0.35);
+ min-width: 220px;
+}
+
+.status-dot {
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
+ background: #64dfdf;
+ box-shadow: 0 0 15px rgba(100, 223, 223, 0.8);
+ animation: pulse 1.6s infinite ease-in-out;
+}
+
+@keyframes pulse {
+ 0% {
+ transform: scale(1);
+ opacity: 1;
+ }
+ 50% {
+ transform: scale(1.2);
+ opacity: 0.7;
+ }
+ 100% {
+ transform: scale(1);
+ opacity: 1;
+ }
}
-main {
+.status-text {
+ margin: 0;
+ font-weight: 600;
+}
+
+.stat-grid {
display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 1rem;
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ margin: 1.25rem 0 1.5rem;
}
-.panel {
- border: 1px solid var(--border-color, #ccc);
- border-radius: 10px;
+.stat-card {
+ padding: 1rem 1.25rem;
+ border-radius: 16px;
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.02));
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ box-shadow: 0 10px 35px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.08);
+ display: flex;
+ flex-direction: column;
+ gap: 0.2rem;
+}
+
+.stat-card strong {
+ font-size: 1.6rem;
+}
+
+.content-grid {
+ display: grid;
+ gap: 1rem;
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
+}
+
+.panel.glass {
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.02));
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ border-radius: 16px;
padding: 1rem;
- background: color-mix(in srgb, var(--panel-color, #f7f7f7), transparent 10%);
- box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
+ box-shadow: 0 18px 50px rgba(0, 0, 0, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.05);
+}
+
+.panel-heading {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 0.5rem;
+ margin-bottom: 0.5rem;
+}
+
+.panel h2 {
+ margin: 0;
}
.field {
display: flex;
flex-direction: column;
- margin-bottom: 0.75rem;
+ margin-bottom: 0.8rem;
}
.field.checkbox {
@@ -48,44 +171,156 @@ label {
input,
select,
-button {
- padding: 0.5rem;
- border-radius: 6px;
- border: 1px solid #ccc;
+button,
+textarea {
+ padding: 0.65rem 0.75rem;
+ border-radius: 10px;
+ border: 1px solid rgba(255, 255, 255, 0.12);
font-size: 1rem;
+ background: rgba(255, 255, 255, 0.05);
+ color: #f4f5ff;
+}
+
+input::placeholder,
+textarea::placeholder {
+ color: rgba(244, 245, 255, 0.65);
}
button {
cursor: pointer;
- background: linear-gradient(90deg, #5e60ce, #64dfdf);
- color: white;
border: none;
- transition: transform 120ms ease, box-shadow 120ms ease;
+ background: linear-gradient(135deg, #ff7a9e, #8b3cf5);
+ color: white;
+ transition: transform 120ms ease, box-shadow 120ms ease, filter 120ms ease;
+ box-shadow: 0 10px 25px rgba(139, 60, 245, 0.4);
}
button:hover {
transform: translateY(-1px);
- box-shadow: 0 6px 14px rgba(0, 0, 0, 0.12);
+ box-shadow: 0 15px 30px rgba(139, 60, 245, 0.45);
+}
+
+button.ghost,
+.pill-button.ghost {
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
+ border: 1px solid rgba(255, 255, 255, 0.16);
+ box-shadow: none;
+}
+
+button.primary {
+ width: 100%;
+}
+
+.pill-button {
+ padding: 0.65rem 1rem;
+ border-radius: 999px;
+ border: 1px solid rgba(255, 255, 255, 0.18);
+ background: linear-gradient(135deg, #6ad7e5, #a16cf6);
+ box-shadow: 0 10px 25px rgba(100, 223, 223, 0.3);
+}
+
+.pill-button.ghost {
+ background: rgba(255, 255, 255, 0.08);
+ box-shadow: none;
+}
+
+.pill {
+ display: inline-block;
+ padding: 0.25rem 0.6rem;
+ border-radius: 999px;
+ font-size: 0.85rem;
+ background: rgba(255, 255, 255, 0.08);
+ border: 1px solid rgba(255, 255, 255, 0.16);
+}
+
+.pill.subtle {
+ color: rgba(244, 245, 255, 0.75);
}
.result {
min-height: 1.5rem;
margin-top: 0.5rem;
- font-size: 0.9rem;
+ font-size: 0.95rem;
+ color: #a7f3d0;
}
-#rooms,
-#session {
+.stack {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
-.room-card {
- border: 1px solid #ccc;
+.inline-fields {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
+ gap: 0.75rem;
+}
+
+.feed-panel {
+ grid-column: 1 / -1;
+}
+
+.feed-list {
+ max-height: 360px;
+ overflow: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+ padding: 0.5rem;
+ border-radius: 14px;
+ background: rgba(255, 255, 255, 0.02);
+ border: 1px solid rgba(255, 255, 255, 0.06);
+}
+
+.feed-item {
+ padding: 0.75rem 0.85rem;
+ border-radius: 12px;
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.02));
+ box-shadow: 0 8px 28px rgba(0, 0, 0, 0.25);
+}
+
+.feed-meta {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-size: 0.9rem;
+ opacity: 0.85;
+}
+
+.feed-body {
+ margin-top: 0.3rem;
+ font-size: 1rem;
+}
+
+.feed-actions {
+ margin-top: 0.4rem;
+ display: flex;
+ gap: 0.4rem;
+}
+
+.feed-pill {
+ padding: 0.2rem 0.55rem;
+ background: rgba(100, 223, 223, 0.16);
border-radius: 8px;
- padding: 0.75rem;
- background: rgba(100, 223, 223, 0.08);
+ border: 1px solid rgba(100, 223, 223, 0.28);
+ font-size: 0.82rem;
+}
+
+.chat-form {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+ gap: 0.5rem;
+ margin-top: 0.85rem;
+}
+
+.room-card {
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ border-radius: 12px;
+ padding: 0.8rem;
+ background: radial-gradient(circle at 20% 20%, rgba(100, 223, 223, 0.12), transparent 45%),
+ linear-gradient(180deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.02));
+ box-shadow: 0 10px 35px rgba(0, 0, 0, 0.35);
}
.room-heading {
@@ -95,8 +330,8 @@ button:hover {
}
.room-meta {
- font-size: 0.85rem;
- opacity: 0.9;
+ font-size: 0.9rem;
+ opacity: 0.85;
}
.room-body {
@@ -107,33 +342,11 @@ button:hover {
.scrollable {
max-height: 260px;
overflow: auto;
- border: 1px solid #ccc;
- border-radius: 8px;
+ border-radius: 10px;
padding: 0.5rem;
background: rgba(0, 0, 0, 0.02);
}
-.message {
- margin-bottom: 0.5rem;
-}
-
-.message .meta {
- font-size: 0.85rem;
- opacity: 0.8;
-}
-
-.session-summary {
- font-size: 0.95rem;
-}
-
-.session-summary h3 {
- margin: 0.25rem 0;
-}
-
-ul {
- padding-left: 1.25rem;
-}
-
.grid {
display: grid;
gap: 0.75rem;
@@ -142,7 +355,7 @@ ul {
.muted {
opacity: 0.8;
- font-size: 0.95rem;
+ color: rgba(244, 245, 255, 0.78);
}
.legend {
@@ -154,29 +367,69 @@ ul {
.legend-item {
padding: 0.5rem;
- border: 1px solid #d0d0d0;
- border-radius: 6px;
- background: rgba(0, 0, 0, 0.02);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 10px;
+ background: rgba(255, 255, 255, 0.02);
}
-.legend-item strong {
- display: block;
+.card {
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ border-radius: 10px;
+ padding: 0.75rem;
+ background: rgba(255, 255, 255, 0.04);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
-.pill {
- display: inline-block;
- padding: 0.15rem 0.5rem;
- border-radius: 999px;
- font-size: 0.8rem;
- background: rgba(94, 96, 206, 0.12);
- border: 1px solid rgba(94, 96, 206, 0.25);
- margin-right: 0.25rem;
+.session-summary h3 {
+ margin: 0.25rem 0;
}
-.card {
- border: 1px solid #d7d7d7;
- border-radius: 8px;
+.session-panel {
+ margin-top: 0.75rem;
padding: 0.75rem;
- background: rgba(94, 96, 206, 0.05);
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
+ border-radius: 12px;
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ background: rgba(255, 255, 255, 0.03);
+}
+
+.stacked-form {
+ margin-bottom: 0.5rem;
+}
+
+.feed-item.room {
+ border-color: rgba(100, 223, 223, 0.35);
+ background: linear-gradient(135deg, rgba(100, 223, 223, 0.14), rgba(255, 255, 255, 0.02));
+ box-shadow: 0 10px 32px rgba(100, 223, 223, 0.18);
+}
+
+.feed-item.chat {
+ border-color: rgba(139, 60, 245, 0.32);
+ box-shadow: 0 10px 32px rgba(139, 60, 245, 0.18);
+}
+
+.feed-tag {
+ font-size: 0.82rem;
+ padding: 0.2rem 0.5rem;
+ border-radius: 8px;
+ background: rgba(255, 255, 255, 0.06);
+ border: 1px solid rgba(255, 255, 255, 0.12);
+}
+
+.full-span {
+ grid-column: 1 / -1;
+}
+
+@media (max-width: 720px) {
+ body {
+ padding: 1rem;
+ }
+
+ .hero {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .feed-panel {
+ grid-column: auto;
+ }
}