All checks were successful
Build & Push Docker Image / build (push) Successful in 6m25s
566 lines
19 KiB
HTML
566 lines
19 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Redlight Meeting</title>
|
|
|
|
<!-- Office.js -->
|
|
<script src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script>
|
|
|
|
<style>
|
|
:root {
|
|
--accent: #6366f1;
|
|
--accent-hover: #4f46e5;
|
|
--bg: #0f1117;
|
|
--bg-s: #1a1d2e;
|
|
--card: #161927;
|
|
--border: #2a2d3e;
|
|
--text: #e2e8f0;
|
|
--text-s: #8892a4;
|
|
--success: #22c55e;
|
|
--error: #ef4444;
|
|
--radius: 10px;
|
|
}
|
|
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font-size: 13px;
|
|
line-height: 1.5;
|
|
padding: 16px;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
h1 { font-size: 18px; font-weight: 700; margin-bottom: 4px; }
|
|
h2 { font-size: 14px; font-weight: 600; margin-bottom: 8px; color: var(--text); }
|
|
.subtitle { font-size: 12px; color: var(--text-s); margin-bottom: 16px; }
|
|
|
|
.card {
|
|
background: var(--card);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
padding: 14px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
input, select {
|
|
width: 100%;
|
|
padding: 8px 12px;
|
|
background: var(--bg-s);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
color: var(--text);
|
|
font-size: 13px;
|
|
outline: none;
|
|
transition: border-color 0.2s;
|
|
}
|
|
input:focus, select:focus {
|
|
border-color: var(--accent);
|
|
}
|
|
input::placeholder { color: var(--text-s); }
|
|
|
|
label {
|
|
display: block;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
color: var(--text);
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.form-group { margin-bottom: 12px; }
|
|
|
|
.btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 6px;
|
|
padding: 8px 16px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
width: 100%;
|
|
}
|
|
.btn-primary {
|
|
background: var(--accent);
|
|
color: white;
|
|
}
|
|
.btn-primary:hover { background: var(--accent-hover); }
|
|
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
|
|
.btn-secondary {
|
|
background: var(--bg-s);
|
|
color: var(--text);
|
|
border: 1px solid var(--border);
|
|
}
|
|
.btn-secondary:hover { background: var(--border); }
|
|
|
|
.btn-success {
|
|
background: var(--success);
|
|
color: white;
|
|
}
|
|
.btn-success:hover { opacity: 0.9; }
|
|
|
|
.btn-sm {
|
|
padding: 6px 12px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.room-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 10px 12px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
margin-bottom: 6px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
.room-item:hover { border-color: var(--accent); background: var(--bg-s); }
|
|
.room-item.selected { border-color: var(--accent); background: var(--accent)20; }
|
|
.room-item .room-name { font-weight: 600; font-size: 13px; }
|
|
.room-item .room-uid { font-size: 11px; color: var(--text-s); }
|
|
|
|
.divider {
|
|
border: none;
|
|
border-top: 1px solid var(--border);
|
|
margin: 16px 0;
|
|
}
|
|
|
|
.error { color: var(--error); font-size: 12px; margin-top: 6px; }
|
|
.success-msg { color: var(--success); font-size: 12px; margin-top: 6px; }
|
|
|
|
.status {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
font-size: 11px;
|
|
padding: 2px 8px;
|
|
border-radius: 99px;
|
|
font-weight: 500;
|
|
}
|
|
.status-online { background: rgba(34,197,94,0.15); color: var(--success); }
|
|
.status-offline { background: rgba(255,255,255,0.06); color: var(--text-s); }
|
|
|
|
.logo {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-bottom: 16px;
|
|
}
|
|
.logo-dot {
|
|
width: 24px;
|
|
height: 24px;
|
|
border-radius: 50%;
|
|
background: var(--accent);
|
|
}
|
|
.logo-text { font-size: 16px; font-weight: 700; }
|
|
|
|
.tabs {
|
|
display: flex;
|
|
gap: 4px;
|
|
background: var(--bg-s);
|
|
border-radius: 8px;
|
|
padding: 3px;
|
|
margin-bottom: 12px;
|
|
}
|
|
.tab {
|
|
flex: 1;
|
|
padding: 6px;
|
|
text-align: center;
|
|
border-radius: 6px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
color: var(--text-s);
|
|
border: none;
|
|
background: transparent;
|
|
transition: all 0.2s;
|
|
}
|
|
.tab.active {
|
|
background: var(--accent);
|
|
color: white;
|
|
}
|
|
|
|
.hidden { display: none !important; }
|
|
.loading { text-align: center; padding: 20px; color: var(--text-s); }
|
|
|
|
.link-preview {
|
|
background: var(--bg-s);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 10px 12px;
|
|
font-size: 12px;
|
|
word-break: break-all;
|
|
color: var(--accent);
|
|
margin-top: 8px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- ─── Login View ─── -->
|
|
<div id="loginView">
|
|
<div class="logo">
|
|
<div class="logo-dot"></div>
|
|
<span class="logo-text">Redlight</span>
|
|
</div>
|
|
<h1>Sign in</h1>
|
|
<p class="subtitle">Connect to your Redlight instance to create meetings.</p>
|
|
|
|
<div class="card">
|
|
<div class="form-group">
|
|
<label>Redlight URL</label>
|
|
<input type="url" id="serverUrl" placeholder="https://your-instance.com" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Email</label>
|
|
<input type="email" id="loginEmail" placeholder="name@example.com" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Password</label>
|
|
<input type="password" id="loginPassword" placeholder="••••••••" />
|
|
</div>
|
|
<div id="loginError" class="error hidden"></div>
|
|
<button class="btn btn-primary" id="loginBtn" onclick="handleLogin()">Sign in</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ─── Main View ─── -->
|
|
<div id="mainView" class="hidden">
|
|
<div class="logo">
|
|
<div class="logo-dot"></div>
|
|
<span class="logo-text">Redlight</span>
|
|
</div>
|
|
|
|
<div class="tabs">
|
|
<button class="tab active" id="tabExisting" onclick="switchTab('existing')">Select Room</button>
|
|
<button class="tab" id="tabCreate" onclick="switchTab('create')">New Room</button>
|
|
</div>
|
|
|
|
<!-- ─── Existing Rooms ─── -->
|
|
<div id="existingPanel">
|
|
<div id="roomsList">
|
|
<div class="loading">Loading rooms...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ─── Create Room ─── -->
|
|
<div id="createPanel" class="hidden">
|
|
<div class="card">
|
|
<div class="form-group">
|
|
<label>Room Name *</label>
|
|
<input type="text" id="newRoomName" placeholder="e.g. Team Meeting" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Access Code</label>
|
|
<input type="text" id="newAccessCode" placeholder="Optional" />
|
|
</div>
|
|
<div id="createError" class="error hidden"></div>
|
|
<button class="btn btn-primary" id="createRoomBtn" onclick="handleCreateRoom()">Create Room</button>
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="divider" />
|
|
|
|
<!-- ─── Selected Room / Insert ─── -->
|
|
<div id="selectedRoomPanel" class="hidden">
|
|
<h2>Selected Room</h2>
|
|
<div class="card">
|
|
<div id="selectedRoomName" style="font-weight:600;margin-bottom:4px;"></div>
|
|
<div id="selectedRoomUid" style="font-size:11px;color:var(--text-s);"></div>
|
|
<div id="meetingLinkPreview" class="link-preview hidden"></div>
|
|
</div>
|
|
<button class="btn btn-success" id="insertBtn" onclick="insertMeetingLink()">
|
|
✔ Insert Meeting Link
|
|
</button>
|
|
<div id="insertSuccess" class="success-msg hidden">Meeting link inserted!</div>
|
|
</div>
|
|
|
|
<div style="margin-top:16px;">
|
|
<button class="btn btn-secondary btn-sm" onclick="handleLogout()">Sign out</button>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// ─── Configuration ──────────────────────────────────────────────────────
|
|
// Will be populated from user input or localStorage
|
|
let REDLIGHT_BASE_URL = '';
|
|
let authToken = '';
|
|
let selectedRoom = null;
|
|
|
|
// ─── Office.js initialization ───────────────────────────────────────────
|
|
Office.onReady(function (info) {
|
|
// Check for cached credentials
|
|
const cached = localStorage.getItem('redlight_outlook');
|
|
if (cached) {
|
|
try {
|
|
const data = JSON.parse(cached);
|
|
REDLIGHT_BASE_URL = data.url;
|
|
authToken = data.token;
|
|
document.getElementById('serverUrl').value = data.url;
|
|
|
|
// Verify token is still valid
|
|
verifyToken().then(valid => {
|
|
if (valid) {
|
|
showMainView();
|
|
loadRooms();
|
|
} else {
|
|
showLoginView();
|
|
}
|
|
});
|
|
} catch {
|
|
showLoginView();
|
|
}
|
|
}
|
|
});
|
|
|
|
// ─── API Helper ─────────────────────────────────────────────────────────
|
|
async function apiRequest(method, path, body) {
|
|
const url = `${REDLIGHT_BASE_URL}/api${path}`;
|
|
const headers = { 'Content-Type': 'application/json' };
|
|
if (authToken) headers['Authorization'] = `Bearer ${authToken}`;
|
|
|
|
const options = { method, headers };
|
|
if (body) options.body = JSON.stringify(body);
|
|
|
|
const res = await fetch(url, options);
|
|
if (!res.ok) {
|
|
const data = await res.json().catch(() => ({}));
|
|
throw new Error(data.error || `HTTP ${res.status}`);
|
|
}
|
|
return res.json();
|
|
}
|
|
|
|
async function verifyToken() {
|
|
try {
|
|
await apiRequest('GET', '/auth/me');
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ─── Login ──────────────────────────────────────────────────────────────
|
|
async function handleLogin() {
|
|
const url = document.getElementById('serverUrl').value.trim().replace(/\/+$/, '');
|
|
const email = document.getElementById('loginEmail').value.trim();
|
|
const password = document.getElementById('loginPassword').value;
|
|
const errorEl = document.getElementById('loginError');
|
|
errorEl.classList.add('hidden');
|
|
|
|
if (!url || !email || !password) {
|
|
errorEl.textContent = 'All fields are required.';
|
|
errorEl.classList.remove('hidden');
|
|
return;
|
|
}
|
|
|
|
REDLIGHT_BASE_URL = url;
|
|
document.getElementById('loginBtn').disabled = true;
|
|
|
|
try {
|
|
const data = await apiRequest('POST', '/auth/login', { email, password });
|
|
authToken = data.token;
|
|
|
|
// Cache credentials
|
|
localStorage.setItem('redlight_outlook', JSON.stringify({ url, token: authToken }));
|
|
|
|
showMainView();
|
|
loadRooms();
|
|
} catch (err) {
|
|
errorEl.textContent = err.message || 'Login failed';
|
|
errorEl.classList.remove('hidden');
|
|
} finally {
|
|
document.getElementById('loginBtn').disabled = false;
|
|
}
|
|
}
|
|
|
|
function handleLogout() {
|
|
authToken = '';
|
|
localStorage.removeItem('redlight_outlook');
|
|
showLoginView();
|
|
}
|
|
|
|
// ─── Views ──────────────────────────────────────────────────────────────
|
|
function showLoginView() {
|
|
document.getElementById('loginView').classList.remove('hidden');
|
|
document.getElementById('mainView').classList.add('hidden');
|
|
}
|
|
|
|
function showMainView() {
|
|
document.getElementById('loginView').classList.add('hidden');
|
|
document.getElementById('mainView').classList.remove('hidden');
|
|
}
|
|
|
|
function switchTab(tab) {
|
|
document.getElementById('tabExisting').classList.toggle('active', tab === 'existing');
|
|
document.getElementById('tabCreate').classList.toggle('active', tab === 'create');
|
|
document.getElementById('existingPanel').classList.toggle('hidden', tab !== 'existing');
|
|
document.getElementById('createPanel').classList.toggle('hidden', tab !== 'create');
|
|
}
|
|
|
|
// ─── Load Rooms ─────────────────────────────────────────────────────────
|
|
async function loadRooms() {
|
|
const container = document.getElementById('roomsList');
|
|
container.innerHTML = '<div class="loading">Loading rooms...</div>';
|
|
|
|
try {
|
|
const data = await apiRequest('GET', '/rooms');
|
|
const rooms = data.rooms || [];
|
|
|
|
if (rooms.length === 0) {
|
|
container.innerHTML = '<div class="loading">No rooms found. Create one first.</div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = rooms.map(r => `
|
|
<div class="room-item" onclick="selectRoom('${r.uid}', '${escapeHtml(r.name)}')" id="room-${r.uid}">
|
|
<div>
|
|
<div class="room-name">${escapeHtml(r.name)}</div>
|
|
<div class="room-uid">${r.uid}</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
} catch (err) {
|
|
container.innerHTML = `<div class="error">${err.message}</div>`;
|
|
}
|
|
}
|
|
|
|
function selectRoom(uid, name) {
|
|
selectedRoom = { uid, name };
|
|
|
|
// Update UI
|
|
document.querySelectorAll('.room-item').forEach(el => el.classList.remove('selected'));
|
|
const el = document.getElementById(`room-${uid}`);
|
|
if (el) el.classList.add('selected');
|
|
|
|
// Show selected panel
|
|
const panel = document.getElementById('selectedRoomPanel');
|
|
panel.classList.remove('hidden');
|
|
document.getElementById('selectedRoomName').textContent = name;
|
|
document.getElementById('selectedRoomUid').textContent = uid;
|
|
document.getElementById('insertSuccess').classList.add('hidden');
|
|
|
|
// Build and show meeting link preview
|
|
const link = `${REDLIGHT_BASE_URL}/join/${uid}`;
|
|
const preview = document.getElementById('meetingLinkPreview');
|
|
preview.textContent = link;
|
|
preview.classList.remove('hidden');
|
|
}
|
|
|
|
// ─── Create Room ────────────────────────────────────────────────────────
|
|
async function handleCreateRoom() {
|
|
const name = document.getElementById('newRoomName').value.trim();
|
|
const accessCode = document.getElementById('newAccessCode').value.trim();
|
|
const errorEl = document.getElementById('createError');
|
|
errorEl.classList.add('hidden');
|
|
|
|
if (!name) {
|
|
errorEl.textContent = 'Room name is required.';
|
|
errorEl.classList.remove('hidden');
|
|
return;
|
|
}
|
|
|
|
document.getElementById('createRoomBtn').disabled = true;
|
|
|
|
try {
|
|
const data = await apiRequest('POST', '/rooms', {
|
|
name,
|
|
access_code: accessCode || null,
|
|
mute_on_join: true,
|
|
record_meeting: true,
|
|
});
|
|
const room = data.room;
|
|
selectRoom(room.uid, room.name);
|
|
switchTab('existing');
|
|
loadRooms(); // Refresh list
|
|
|
|
// Clear form
|
|
document.getElementById('newRoomName').value = '';
|
|
document.getElementById('newAccessCode').value = '';
|
|
} catch (err) {
|
|
errorEl.textContent = err.message || 'Could not create room';
|
|
errorEl.classList.remove('hidden');
|
|
} finally {
|
|
document.getElementById('createRoomBtn').disabled = false;
|
|
}
|
|
}
|
|
|
|
// ─── Insert Meeting Link into Outlook Event ─────────────────────────────
|
|
function insertMeetingLink() {
|
|
if (!selectedRoom) return;
|
|
|
|
const joinUrl = `${REDLIGHT_BASE_URL}/join/${selectedRoom.uid}`;
|
|
const meetingHtml = `
|
|
<br/>
|
|
<div style="border-left: 3px solid #6366f1; padding: 8px 14px; margin: 12px 0; background: #f8f9fa; border-radius: 4px;">
|
|
<strong style="color:#6366f1;">Redlight Meeting</strong><br/>
|
|
<strong>${escapeHtml(selectedRoom.name)}</strong><br/>
|
|
<a href="${joinUrl}" style="color:#6366f1;">${joinUrl}</a>
|
|
</div>
|
|
`;
|
|
|
|
try {
|
|
const item = Office.context.mailbox.item;
|
|
|
|
// Set location
|
|
if (item.location && item.location.setAsync) {
|
|
item.location.setAsync(joinUrl);
|
|
}
|
|
|
|
// Append meeting link to body
|
|
item.body.getTypeAsync(function (result) {
|
|
if (result.status === Office.AsyncResultStatus.Succeeded) {
|
|
const bodyType = result.value;
|
|
if (bodyType === Office.CoercionType.Html) {
|
|
item.body.setSelectedDataAsync(meetingHtml, { coercionType: Office.CoercionType.Html }, function (r) {
|
|
if (r.status === Office.AsyncResultStatus.Succeeded) {
|
|
showInsertSuccess();
|
|
} else {
|
|
// Fallback: append at end
|
|
item.body.prependAsync(meetingHtml, { coercionType: Office.CoercionType.Html }, function () {
|
|
showInsertSuccess();
|
|
});
|
|
}
|
|
});
|
|
} else {
|
|
// Plain text fallback
|
|
const textContent = `\n\nRedlight Meeting: ${selectedRoom.name}\n${joinUrl}\n`;
|
|
item.body.setSelectedDataAsync(textContent, { coercionType: Office.CoercionType.Text }, function () {
|
|
showInsertSuccess();
|
|
});
|
|
}
|
|
}
|
|
});
|
|
} catch (err) {
|
|
// If Office.js is not available (testing outside Outlook)
|
|
console.log('Meeting link:', joinUrl);
|
|
showInsertSuccess();
|
|
}
|
|
}
|
|
|
|
function showInsertSuccess() {
|
|
document.getElementById('insertSuccess').classList.remove('hidden');
|
|
document.getElementById('insertBtn').textContent = '✓ Inserted!';
|
|
setTimeout(() => {
|
|
document.getElementById('insertBtn').innerHTML = '✔ Insert Meeting Link';
|
|
}, 3000);
|
|
}
|
|
|
|
// ─── Utilities ──────────────────────────────────────────────────────────
|
|
function escapeHtml(str) {
|
|
const div = document.createElement('div');
|
|
div.textContent = str;
|
|
return div.innerHTML;
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|