Files
redlight/outlook-addin/taskpane.html
2026-03-02 10:35:01 +01:00

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()">
&#x2714; 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 = '&#x2714; Insert Meeting Link';
}, 3000);
}
// ─── Utilities ──────────────────────────────────────────────────────────
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
</script>
</body>
</html>