[mirotalk] - improve active rooms page UI/UX

This commit is contained in:
Miroslav Pejic
2026-04-13 01:58:41 +02:00
parent 8517328e36
commit e6ca71febc
10 changed files with 363 additions and 170 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
# ====================================================
# MiroTalk P2P v.1.8.00 - Environment Configuration
# MiroTalk P2P v.1.8.01 - Environment Configuration
# ====================================================
# App environment
+1 -1
View File
@@ -2,7 +2,7 @@
/**
* ==============================================
* MiroTalk P2P v.1.8.00 - Configuration File
* MiroTalk P2P v.1.8.01 - Configuration File
* ==============================================
*
* This file is the central configuration source.
+1 -1
View File
@@ -45,7 +45,7 @@ dependencies: {
* @license For commercial use or closed source, contact us at license.mirotalk@gmail.com or purchase directly from CodeCanyon
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-p2p-webrtc-realtime-video-conferences/38376661
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
* @version 1.8.00
* @version 1.8.01
*
*/
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "mirotalk",
"version": "1.8.00",
"version": "1.8.01",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mirotalk",
"version": "1.8.00",
"version": "1.8.01",
"license": "AGPL-3.0",
"dependencies": {
"@mattermost/client": "11.5.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mirotalk",
"version": "1.8.00",
"version": "1.8.01",
"description": "A free WebRTC browser-based video call",
"main": "server.js",
"scripts": {
+291 -139
View File
@@ -1,195 +1,347 @@
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@500&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap');
:root {
--primary-bg: #1e2229;
--card-bg: #15181d;
--accent: #1870d7;
--accent2: #2186ff;
--text: #f6f8fa;
--card-shadow: 0 2px 8px #15181d;
--primary-bg: #0f1117;
--container-bg: #181a20;
--card-bg: #1e2128;
--card-bg-hover: #252830;
--accent: #3b82f6;
--accent-hover: #2563eb;
--accent-glow: rgba(59, 130, 246, 0.15);
--text: #e8ecf1;
--text-muted: #8b95a5;
--border: #2a2d35;
--success: #22c55e;
--gold: #fbbf24;
--card-shadow: 0 4px 24px rgba(0, 0, 0, 0.25);
--transition: 0.2s ease;
}
* {
box-sizing: border-box;
}
body {
font-family: 'Montserrat';
font-family: 'Montserrat', sans-serif;
background: var(--primary-bg);
margin: 0;
padding: 0;
color: var(--text);
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 40px auto;
background: var(--card-bg);
border-radius: 12px;
box-shadow: var(--card-shadow);
padding: 32px;
max-width: 1280px;
margin: 0 auto;
padding: 32px 24px;
}
h1 {
text-align: center;
color: var(--accent2);
margin-bottom: 32px;
letter-spacing: 1px;
}
.search-bar {
/* Header */
.header {
display: flex;
justify-content: center;
align-items: center;
justify-content: space-between;
margin-bottom: 28px;
}
.search-input {
padding: 10px 16px;
border-radius: 8px 0 0 8px;
border: none;
font-size: 1.1em;
background: #23262e;
color: var(--text);
outline: none;
width: 220px;
}
.search-btn {
padding: 10px 20px;
border-radius: 0 8px 8px 0;
border: none;
background: var(--accent);
color: #fff;
font-size: 1.1em;
cursor: pointer;
font-weight: bold;
transition: background 0.2s;
}
.search-btn:hover {
background: var(--accent2);
}
.rooms {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 28px;
justify-content: center;
}
.room-card {
background: var(--accent);
border-radius: 12px;
box-shadow: 0 1px 6px #15181d;
padding: 32px 24px;
min-height: 220px;
text-align: center;
transition:
box-shadow 0.2s,
transform 0.2s;
color: var(--text);
position: relative;
.header-left {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
gap: 12px;
}
.room-card:hover {
cursor: pointer;
transform: translateY(-4px) scale(1.03);
.header-left i {
font-size: 1.5em;
color: var(--accent);
}
.room-title {
font-size: 1.15em;
color: #f6f8fa;
margin-bottom: 14px;
font-weight: bold;
letter-spacing: 0.5px;
word-break: break-all;
overflow-wrap: anywhere;
max-width: 100%;
.header h1 {
font-size: 1.5em;
font-weight: 700;
color: var(--text);
margin: 0;
letter-spacing: -0.3px;
}
.peer-count {
font-size: 2.3em;
color: #ffd700;
font-weight: bold;
margin-bottom: 8px;
margin-top: auto;
align-self: center;
}
.peer-label {
font-size: 0.9em;
color: #f6f8fa;
opacity: 0.7;
margin-bottom: 60px;
align-self: center;
}
.empty {
text-align: center;
color: #b3c6e0;
margin-top: 40px;
font-size: 1.2em;
.room-count-badge {
background: var(--accent-glow);
color: var(--accent);
font-size: 0.75em;
font-weight: 600;
padding: 4px 10px;
border-radius: 20px;
border: 1px solid rgba(59, 130, 246, 0.25);
}
.refresh-btn {
display: block;
margin: 0 auto 32px auto;
padding: 10px 28px;
background: var(--accent2);
color: #fff;
border: none;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
background: var(--card-bg);
color: var(--text-muted);
border: 1px solid var(--border);
border-radius: 10px;
cursor: pointer;
font-size: 1.1em;
font-weight: bold;
letter-spacing: 0.5px;
box-shadow: 0 1px 4px #15181d;
transition: background 0.2s;
font-size: 1em;
transition: all var(--transition);
}
.refresh-btn:hover {
background: var(--accent);
color: #fff;
border-color: var(--accent);
}
.refresh-btn.spinning i {
animation: spin 0.6s ease;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Search */
.search-bar {
position: relative;
margin-bottom: 28px;
}
.search-bar i {
position: absolute;
left: 14px;
top: 50%;
transform: translateY(-50%);
color: var(--text-muted);
font-size: 0.9em;
pointer-events: none;
}
.search-input {
width: 100%;
padding: 12px 16px 12px 40px;
border-radius: 10px;
border: 1px solid var(--border);
font-size: 0.95em;
font-family: 'Montserrat', sans-serif;
background: var(--card-bg);
color: var(--text);
outline: none;
transition:
border-color var(--transition),
box-shadow var(--transition);
}
.search-input::placeholder {
color: var(--text-muted);
}
.search-input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-glow);
}
/* Stats summary */
.stats-bar {
display: flex;
gap: 16px;
margin-bottom: 28px;
}
.stat-item {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 12px;
padding: 16px 24px;
flex: 1;
text-align: center;
}
.stat-value {
font-size: 1.8em;
font-weight: 700;
color: var(--text);
line-height: 1.2;
}
.stat-value.accent {
color: var(--accent);
}
.stat-value.gold {
color: var(--gold);
}
.stat-label {
font-size: 0.75em;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 1px;
margin-top: 4px;
}
/* Room grid */
.rooms {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
.room-card {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 14px;
padding: 20px;
transition:
background 0.2s ease,
border-color 0.2s ease,
transform 0.2s ease;
color: var(--text);
display: flex;
flex-direction: column;
gap: 16px;
}
.room-card:hover {
background: var(--card-bg-hover);
border-color: var(--accent);
transform: translateY(-2px);
}
.room-card-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.room-title {
font-size: 0.85em;
color: var(--text);
font-weight: 600;
word-break: break-all;
overflow-wrap: anywhere;
line-height: 1.4;
flex: 1;
}
.room-title i {
color: var(--accent);
margin-right: 6px;
}
.peer-badge {
display: flex;
align-items: center;
gap: 6px;
background: var(--accent-glow);
color: var(--accent);
padding: 4px 10px;
border-radius: 20px;
font-size: 0.8em;
font-weight: 600;
white-space: nowrap;
border: 1px solid rgba(59, 130, 246, 0.2);
}
.peer-badge i {
font-size: 0.85em;
}
.room-card-footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: auto;
}
.peer-status {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.8em;
color: var(--text-muted);
}
.peer-status .dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--success);
display: inline-block;
transition: box-shadow 0.3s ease;
}
.join-btn {
display: block;
margin: 0 auto 0 auto;
position: absolute;
left: 50%;
bottom: 18px;
transform: translateX(-50%);
padding: 10px 24px;
background: var(--accent2);
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 18px;
background: var(--accent);
color: #fff;
border-radius: 8px;
font-size: 1em;
font-weight: bold;
font-size: 0.85em;
font-weight: 600;
text-decoration: none;
transition: background 0.2s;
box-shadow: 0 1px 4px #15181d;
transition: all var(--transition);
font-family: 'Montserrat', sans-serif;
}
.join-btn:hover {
background: var(--accent);
background: var(--accent-hover);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.35);
}
/* Empty state */
.empty {
text-align: center;
color: var(--text-muted);
padding: 60px 20px;
font-size: 1em;
grid-column: 1 / -1;
}
.empty i {
display: block;
font-size: 2.5em;
margin-bottom: 16px;
opacity: 0.4;
}
/* Responsive */
@media (max-width: 900px) {
.container {
max-width: 98vw;
padding: 16px;
padding: 20px 16px;
}
.rooms {
gap: 16px;
.stats-bar {
gap: 10px;
}
.stat-item {
padding: 12px 16px;
}
.stat-value {
font-size: 1.4em;
}
}
@media (max-width: 600px) {
.header {
flex-wrap: wrap;
gap: 12px;
}
.stats-bar {
flex-direction: column;
}
.rooms {
grid-template-columns: 1fr;
}
.room-card {
padding: 20px 10px;
}
.peer-label {
margin-bottom: 70px;
.header h1 {
font-size: 1.2em;
}
}
+40 -17
View File
@@ -6,13 +6,20 @@ const isTest = false; // Set to true for testing with mock data
const roomsDiv = document.getElementById('rooms');
const searchInput = document.getElementById('searchInput');
const searchBtn = document.getElementById('search-btn');
const refreshBtn = document.getElementById('refresh-btn');
const roomCountBadge = document.getElementById('roomCountBadge');
const statRooms = document.getElementById('statRooms');
const statPeers = document.getElementById('statPeers');
let allRooms = [];
searchBtn.addEventListener('click', handleSearch);
refreshBtn.addEventListener('click', fetchRooms);
searchInput.addEventListener('input', handleSearch);
refreshBtn.addEventListener('click', () => {
refreshBtn.classList.add('spinning');
fetchRooms().finally(() => {
setTimeout(() => refreshBtn.classList.remove('spinning'), 600);
});
});
function setRoomsContent(html) {
roomsDiv.innerHTML = html;
@@ -39,22 +46,31 @@ function getUUID() {
);
}
function updateStats(rooms) {
const totalPeers = rooms.reduce((sum, r) => sum + r.peers, 0);
statRooms.textContent = rooms.length;
statPeers.textContent = totalPeers;
roomCountBadge.textContent = rooms.length === 1 ? '1 room' : `${rooms.length} rooms`;
}
async function fetchRooms() {
setRoomsContent('<div class="empty">Loading...</div>');
setRoomsContent('<div class="empty"><i class="fa-solid fa-spinner fa-spin"></i>Loading rooms...</div>');
try {
const res = await axios.get('/api/v1/activeRooms');
if (res.status !== 200) throw new Error('Failed to fetch active rooms');
allRooms = getRoomsData(res);
updateStats(allRooms);
renderRooms(allRooms);
} catch (err) {
const errorMsg = err.response?.data?.error || err.message;
setRoomsContent(`<div class="empty">${errorMsg}</div>`);
setRoomsContent(`<div class="empty"><i class="fa-solid fa-circle-exclamation"></i>${errorMsg}</div>`);
updateStats([]);
}
}
function renderRooms(rooms) {
if (!rooms.length) {
setRoomsContent('<div class="empty">No active rooms found.</div>');
setRoomsContent('<div class="empty"><i class="fa-solid fa-door-closed"></i>No active rooms found.</div>');
return;
}
setRoomsContent(
@@ -62,18 +78,24 @@ function renderRooms(rooms) {
.map(
(room) => `
<div class="room-card">
<div class="room-title">
<i class="fa-solid fa-door-open"></i>
${room.id}
<div class="room-card-header">
<div class="room-title">
<i class="fa-solid fa-door-open"></i>${room.id}
</div>
<div class="peer-badge">
<i class="fa-solid fa-users"></i>
${room.peers}
</div>
</div>
<div class="peer-count">
<i class="fa-solid fa-users"></i>
${room.peers}
<div class="room-card-footer">
<div class="peer-status">
<span class="dot"></span>
${room.peers} ${room.peers === 1 ? 'peer' : 'peers'} connected
</div>
<a href="${room.join}" class="join-btn" target="_blank">
<i class="fa-solid fa-arrow-right-to-bracket"></i> Join
</a>
</div>
<div class="peer-label">${room.peers === 1 ? 'peer' : 'peers'}</div>
<a href="${room.join}" class="join-btn" target="_blank">
<i class="fa-solid fa-sign-in-alt"></i> Join
</a>
</div>
`
)
@@ -83,7 +105,8 @@ function renderRooms(rooms) {
function handleSearch() {
const value = searchInput.value.trim().toLowerCase();
renderRooms(!value ? allRooms : allRooms.filter((room) => room.id.toLowerCase().includes(value)));
const filtered = !value ? allRooms : allRooms.filter((room) => room.id.toLowerCase().includes(value));
renderRooms(filtered);
}
fetchRooms();
+1 -1
View File
@@ -109,7 +109,7 @@ let brand = {
},
about: {
imageUrl: '../images/mirotalk-logo.gif',
title: 'WebRTC P2P v1.8.00',
title: 'WebRTC P2P v1.8.01',
html: `
<button
id="support-button"
+2 -2
View File
@@ -15,7 +15,7 @@
* @license For commercial use or closed source, contact us at license.mirotalk@gmail.com or purchase directly from CodeCanyon
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-p2p-webrtc-realtime-video-conferences/38376661
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
* @version 1.8.00
* @version 1.8.01
*
*/
@@ -14966,7 +14966,7 @@ function showAbout() {
Swal.fire({
background: swBg,
position: 'center',
title: brand.about?.title && brand.about.title.trim() !== '' ? brand.about.title : 'WebRTC P2P v1.8.00',
title: brand.about?.title && brand.about.title.trim() !== '' ? brand.about.title : 'WebRTC P2P v1.8.01',
imageUrl: brand.about?.imageUrl && brand.about.imageUrl.trim() !== '' ? brand.about.imageUrl : images.about,
customClass: { image: 'img-about' },
html: `
+23 -5
View File
@@ -24,12 +24,30 @@
</head>
<body>
<div class="container">
<h1>Active Rooms</h1>
<div class="search-bar">
<input class="search-input" id="searchInput" type="text" placeholder="Search by Room ID..." />
<button id="search-btn" class="search-btn">Search</button>
<div class="header">
<div class="header-left">
<i class="fa-solid fa-tower-broadcast"></i>
<h1>Active Rooms</h1>
<span class="room-count-badge" id="roomCountBadge">0 rooms</span>
</div>
<button id="refresh-btn" class="refresh-btn" title="Refresh">
<i class="fa-solid fa-arrows-rotate"></i>
</button>
</div>
<div class="search-bar">
<i class="fa-solid fa-magnifying-glass"></i>
<input class="search-input" id="searchInput" type="text" placeholder="Search rooms..." />
</div>
<div class="stats-bar" id="statsBar">
<div class="stat-item">
<div class="stat-value accent" id="statRooms">0</div>
<div class="stat-label">Rooms</div>
</div>
<div class="stat-item">
<div class="stat-value gold" id="statPeers">0</div>
<div class="stat-label">Total Peers</div>
</div>
</div>
<button id="refresh-btn" class="refresh-btn">Refresh</button>
<div class="rooms" id="rooms"></div>
</div>
<script defer src="../js/activeRooms.js"></script>