feat: BizGaze Connect home, BizGaze login, modular backend, /api/v1
User-facing - New post-login home (/home): chat rail + Share/Connect (embedded) + Meeting; login lives here when logged out - Landing: "Log in with BizGaze" + no-login screen share - Console replaced by a role-scoped Dashboard (/dashboard): admins see all team sessions, others see only their own; stats + CSV/PDF export - Recordings saved as MP4 (H.264/AAC) with WebM fallback; old .webm still downloadable - Fix: duplicate "Sign in" on the login card Auth / integration - BizGaze as identity provider: /api/login validates against BIZGAZE_LOGIN_URL (env-gated) and provisions a local user - Phase 2 start: /api/v1 alias for all /api routes; Authorization: Bearer accepted across HTTP + WS; login returns a token (for native desktop/mobile clients) Backend refactor (Phase 1, behavior-preserving) - Split server.js into config/lib/session/presence/routes/static/signaling + repos (data-access) + bizgaze (service) - All SQL behind repos.js, tenant-scoped (tenantId == team_id for now) - e2e updated to current flow (21/21 pass before and after) Docs: ARCHITECTURE.md (target architecture + phased plan), CLAUDE.md repo layout, .env.example BIZGAZE_LOGIN_URL Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -43,9 +43,20 @@
|
||||
.pwwrap input{padding-right:2.7rem;}
|
||||
.eye{position:absolute;right:.5rem;top:50%;transform:translateY(-50%);background:none;border:none;padding:.3rem;width:auto;color:var(--muted);display:inline-flex;align-items:center;cursor:pointer;margin:0;}
|
||||
.eye:hover{color:var(--blue);}
|
||||
#homeLink{position:fixed;top:14px;left:16px;z-index:50;display:inline-flex;align-items:center;gap:6px;background:rgba(255,255,255,.92);color:#1F3B73;text-decoration:none;font-weight:600;font-size:.86rem;padding:.45rem .8rem;border-radius:10px;box-shadow:0 4px 12px rgba(0,0,0,.15);}
|
||||
.formerr{color:#b91c1c;font-weight:600;font-size:.88rem;margin-top:.9rem;min-height:1.1em;text-align:left;}
|
||||
.formerr.show{display:flex;align-items:center;gap:.5rem;background:#fee2e2;border:1px solid #fca5a5;border-radius:9px;padding:.6rem .75rem;animation:errShake .35s;}
|
||||
.formerr.show::before{content:"⚠";font-size:1rem;}
|
||||
@keyframes errShake{0%,100%{transform:translateX(0)}20%,60%{transform:translateX(-5px)}40%,80%{transform:translateX(5px)}}
|
||||
/* Embedded inside the home shell: hide own chrome (the shell provides it). */
|
||||
html.embed .topbar{display:none!important;}
|
||||
html.embed #homeLink{display:none!important;}
|
||||
html.embed #video{height:100vh!important;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script>if(new URLSearchParams(location.search).get('embed')==='1')document.documentElement.classList.add('embed');</script>
|
||||
<a href="/home" id="homeLink">← Home</a>
|
||||
<div class="topbar" id="topbar">
|
||||
<div class="brandrow"><img src="/logo.png" alt="" style="height:46px;width:auto;max-width:190px;border-radius:8px;object-fit:contain;background:#fff;padding:5px 12px;image-rendering:-webkit-optimize-contrast" onerror="this.replaceWith(Object.assign(document.createElement('div'),{className:'logo',textContent:'B'}))"><div class="brand">BizGaze <span>Support</span></div></div>
|
||||
<div class="agentchip" id="agentChip"></div>
|
||||
@@ -60,7 +71,10 @@ const IS_MOBILE=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|M
|
||||
let __icePromise=Promise.resolve();try{__icePromise=fetch('/api/ice').then(r=>r.ok?r.json():null).then(c=>{if(c&&c.iceServers&&IS_MOBILE)ICE=c;}).catch(()=>{});}catch(_){}
|
||||
async function ensureIce(){try{await __icePromise;}catch(_){}return ICE;}
|
||||
function pEsc(s){return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));}
|
||||
function profileHTML(name){return '<div class="profile"><button class="pbtn" id="pbtn">'+pEsc(name)+' <span style="font-size:.65rem">▾</span></button><div class="pmenu" id="pmenu"><a href="/console">Console / Dashboard</a><a id="plogout" class="danger">Logout</a></div></div>';}
|
||||
// When embedded in the home shell, tell the parent when a session is live so the
|
||||
// rail can show a "return here" indicator.
|
||||
function bzcSession(active){try{if(window.parent&&window.parent!==window)window.parent.postMessage({type:'bzc-session',flow:'connect',active:!!active},location.origin);}catch(_){}}
|
||||
function profileHTML(name){return '<div class="profile"><button class="pbtn" id="pbtn">'+pEsc(name)+' <span style="font-size:.65rem">▾</span></button><div class="pmenu" id="pmenu"><a href="/home">Home</a><a href="/dashboard">Dashboard</a><a id="plogout" class="danger">Logout</a></div></div>';}
|
||||
function wireProfile(){const btn=document.getElementById('pbtn'),menu=document.getElementById('pmenu');if(!btn)return;btn.onclick=(e)=>{e.stopPropagation();menu.classList.toggle('open');};document.addEventListener('click',()=>menu.classList.remove('open'));const lo=document.getElementById('plogout');if(lo)lo.onclick=async()=>{try{await fetch('/api/logout',{method:'POST'});}catch(_){}location.href='/';};}
|
||||
function makeBrandClickable(){document.querySelectorAll('.brandrow,.wordmark').forEach(el=>{el.style.cursor='pointer';el.addEventListener('click',()=>{location.href='/';});});}
|
||||
makeBrandClickable();
|
||||
@@ -99,13 +113,14 @@ function renderLogin(){
|
||||
<span class="lbl">Password</span><div class="pwwrap"><input id="pw" type="password" placeholder="password"><button type="button" class="eye" data-for="pw" aria-label="Show password"></button></div>
|
||||
<label style="display:flex;align-items:center;gap:.5rem;margin:.7rem 0;color:var(--ink);font-size:.9rem;cursor:pointer"><input type="checkbox" id="rememberMe" style="width:18px;height:18px;accent-color:var(--blue);margin:0"> Remember me on this device</label>
|
||||
<button class="btn" id="loginBtn" style="width:100%">Sign in</button>
|
||||
<div class="status err" id="err"></div>`;
|
||||
<div class="formerr" id="err"></div>`;
|
||||
{
|
||||
const doSignIn=async()=>{
|
||||
const errEl=document.getElementById('err'); errEl.textContent=''; errEl.classList.remove('show');
|
||||
try{
|
||||
await api('/api/login',{email:document.getElementById('email').value,password:document.getElementById('pw').value,remember:(document.getElementById('rememberMe')||{}).checked||false});
|
||||
me=await api('/api/me',null,'GET'); renderAgent();
|
||||
}catch(e){ document.getElementById('err').textContent=e.message; }
|
||||
}catch(e){ errEl.textContent=/invalid credentials/i.test(e.message)?'Incorrect email or password. Please try again.':e.message; errEl.classList.add('show'); }
|
||||
};
|
||||
document.getElementById('loginBtn').onclick=doSignIn;
|
||||
onEnter(['email','pw'], doSignIn);
|
||||
@@ -173,11 +188,13 @@ function renderWaiting(){
|
||||
}
|
||||
|
||||
function renderEnded(msg){
|
||||
bzcSession(false);
|
||||
try{ stopRecording(); }catch(_){}
|
||||
removeSessionUI();
|
||||
if(pc){ try{pc.close();}catch(e){} pc=null; }
|
||||
video.style.display='none'; bar.classList.remove('show');
|
||||
topbar.style.display='flex'; wrap.style.display='grid';
|
||||
{ const hl=document.getElementById('homeLink'); if(hl && !document.documentElement.classList.contains('embed')) hl.style.display=''; }
|
||||
card.innerHTML=`
|
||||
<h1>Session ended</h1>
|
||||
<div class="sub">${esc(msg)}</div>
|
||||
@@ -251,12 +268,17 @@ function startRecording(){
|
||||
const mixed=new MediaStream();
|
||||
mixed.addTrack(remote.getVideoTracks()[0]);
|
||||
dest.stream.getAudioTracks().forEach(t=>mixed.addTrack(t));
|
||||
let mime='video/webm;codecs=vp8,opus'; if(!(window.MediaRecorder&&MediaRecorder.isTypeSupported(mime))) mime='video/webm';
|
||||
// Prefer MP4 (H.264/AAC) — playable by most tools (Windows Media Player, QuickTime,
|
||||
// WhatsApp, etc.). Fall back to WebM only if the browser can't record MP4.
|
||||
const REC_TYPES=['video/mp4;codecs=avc1.42E01E,mp4a.40.2','video/mp4;codecs=h264,aac','video/mp4','video/webm;codecs=vp8,opus','video/webm'];
|
||||
let mime='video/webm'; for(const t of REC_TYPES){ if(window.MediaRecorder&&MediaRecorder.isTypeSupported(t)){ mime=t; break; } }
|
||||
const recExt = mime.indexOf('mp4')!==-1 ? 'mp4' : 'webm';
|
||||
const recBlobType = mime.indexOf('mp4')!==-1 ? 'video/mp4' : 'video/webm';
|
||||
recChunks=[];
|
||||
mediaRecorder=new MediaRecorder(mixed,{mimeType:mime});
|
||||
mediaRecorder.ondataavailable=(e)=>{ if(e.data&&e.data.size) recChunks.push(e.data); };
|
||||
mediaRecorder.onstop=async()=>{
|
||||
try{ const blob=new Blob(recChunks,{type:'video/webm'}); if(blob.size) await fetch('/api/recording?sessionId='+encodeURIComponent(sessionId),{method:'POST',headers:{'Content-Type':'video/webm'},body:blob}); }catch(_){}
|
||||
try{ const blob=new Blob(recChunks,{type:recBlobType}); if(blob.size) await fetch('/api/recording?sessionId='+encodeURIComponent(sessionId)+'&ext='+recExt,{method:'POST',headers:{'Content-Type':recBlobType},body:blob}); }catch(_){}
|
||||
try{recCtx&&recCtx.close();}catch(_){} recCtx=null;
|
||||
};
|
||||
mediaRecorder.start(1000);
|
||||
@@ -277,6 +299,8 @@ function stopRecording(){
|
||||
function _btn(id,svg,label,bg){const b=document.createElement('button');b.id=id;b.innerHTML='<span style="display:inline-flex">'+svg+'</span><span>'+label+'</span>';b.style.cssText='display:inline-flex;align-items:center;gap:7px;border:none;border-radius:12px;padding:.62rem 1rem;font-weight:600;font-size:.9rem;cursor:pointer;color:#fff;background:'+bg+';transition:background .15s,transform .08s';b.onmouseenter=()=>b.style.transform='translateY(-1px)';b.onmouseleave=()=>b.style.transform='none';return b;}
|
||||
function buildBar(){
|
||||
if(document.getElementById('sessionBar'))return;
|
||||
{ const hl=document.getElementById('homeLink'); if(hl) hl.style.display='none'; }
|
||||
bzcSession(true);
|
||||
const bar=document.createElement('div'); bar.id='sessionBar';
|
||||
bar.style.cssText='position:fixed;left:50%;bottom:18px;transform:translateX(-50%);z-index:2147483000;display:flex;gap:10px;align-items:center;background:rgba(15,23,42,.94);padding:8px 12px;border-radius:16px;box-shadow:0 10px 28px rgba(0,0,0,.35)';
|
||||
const mic=_btn('micBtn',SVG_MIC,'Mic','#2563eb');
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>BizGaze Support — Staff Console</title>
|
||||
<title>BizGaze Connect — Dashboard</title>
|
||||
<style>
|
||||
:root{ --brand:#FFC708; --brand-d:#E0AC00; --blue:#1F3B73; --blue-d:#16294f; --blue-soft:#EAF0FB; --ink:#1f2430; --muted:#6b7280; --bg:#f6f8fb; --card:#fff; --line:#e6e9ef; --green:#16a34a; --red:#b91c1c; }
|
||||
*{box-sizing:border-box;}
|
||||
@@ -11,8 +11,7 @@
|
||||
header{background:var(--blue);padding:.75rem 1.5rem;display:flex;justify-content:space-between;align-items:center;}
|
||||
.brandrow{display:flex;align-items:center;gap:.6rem;}
|
||||
.logo{width:30px;height:30px;border-radius:8px;background:var(--brand);display:grid;place-items:center;font-weight:800;color:var(--blue);}
|
||||
.brand{font-weight:700;color:#fff;font-size:1.05rem;} .brand span{color:var(--brand);font-weight:600;}
|
||||
.who{color:#dbe4f5;font-size:.85rem;margin-right:.7rem;}
|
||||
.brand{font-weight:700;color:#fff;font-size:1.05rem;} .brand span.y{color:var(--brand);font-weight:700;} .brand span.tag{color:#8ea3cf;font-weight:500;font-size:.85rem;}
|
||||
main{max-width:1020px;margin:1.8rem auto;padding:0 1rem;}
|
||||
.card{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:1.5rem;margin-bottom:1.25rem;box-shadow:0 6px 18px rgba(20,30,60,.05);}
|
||||
h2{font-size:1rem;margin:0 0 1rem;color:var(--blue);}
|
||||
@@ -20,27 +19,19 @@
|
||||
input:focus,select:focus{outline:none;border-color:var(--brand);}
|
||||
button{padding:.6rem 1.1rem;background:var(--brand);color:var(--ink);border:none;border-radius:10px;font-weight:700;cursor:pointer;font-size:.92rem;}
|
||||
button:hover{background:var(--brand-d);}
|
||||
button.ghost{background:transparent;color:#dbe4f5;border:1px solid #46598c;font-weight:600;}
|
||||
button.ghost:hover{background:var(--blue-d);}
|
||||
button.mini{padding:.32rem .6rem;font-size:.76rem;font-weight:600;background:#eef1f6;color:var(--blue);border:1px solid var(--line);}
|
||||
button.mini:hover{background:var(--blue-soft);}
|
||||
button.mini.danger{color:var(--red);}
|
||||
.row{display:flex;gap:.5rem;align-items:center;}
|
||||
.muted{color:var(--muted);font-size:.85rem;}
|
||||
table{width:100%;border-collapse:collapse;font-size:.88rem;}
|
||||
th{color:var(--muted);font-weight:600;font-size:.76rem;text-transform:uppercase;letter-spacing:.04em;}
|
||||
th,td{text-align:left;padding:.55rem .5rem;border-bottom:1px solid var(--line);}
|
||||
.pill{font-size:.74rem;font-weight:600;padding:.15rem .55rem;border-radius:99px;}
|
||||
.pill.on{background:#ecfdf3;color:#15803d;} .pill.off{background:#fee2e2;color:var(--red);}
|
||||
.pill.on{background:#ecfdf3;color:#15803d;}
|
||||
.hidden{display:none;}
|
||||
.tabs{display:flex;gap:.5rem;margin-bottom:1.2rem;}
|
||||
.tabs button{background:#eef1f6;color:var(--muted);font-weight:600;}
|
||||
.tabs button.active{background:var(--blue);color:#fff;}
|
||||
.quick{display:flex;align-items:center;justify-content:space-between;gap:1rem;background:linear-gradient(120deg,var(--blue),var(--blue-d));color:#fff;border:none;}
|
||||
.quick h2{color:#fff;margin:0 0 .25rem;}
|
||||
.quick p{margin:0;color:#cdd7ee;font-size:.88rem;}
|
||||
.quick a{background:var(--brand);color:var(--ink);text-decoration:none;font-weight:700;padding:.7rem 1.3rem;border-radius:10px;white-space:nowrap;}
|
||||
.quick a:hover{background:var(--brand-d);}
|
||||
.lbl{display:block;font-size:.74rem;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin:.7rem 0 .15rem;}
|
||||
.filters{display:flex;gap:.6rem;align-items:flex-end;flex-wrap:wrap;margin-bottom:1rem;}
|
||||
.filters .f{flex:1;min-width:140px;}
|
||||
@@ -50,15 +41,26 @@
|
||||
.pager button{padding:.32rem .7rem;font-size:.8rem;font-weight:600;background:#eef1f6;color:var(--blue);border:1px solid var(--line);}
|
||||
.pager button:hover:not(:disabled){background:var(--blue-soft);}
|
||||
.pager button:disabled{opacity:.4;cursor:default;}
|
||||
.pwwrap{position:relative;}
|
||||
.pwwrap input{padding-right:2.6rem;}
|
||||
.stats{display:flex;gap:1rem;flex-wrap:wrap;margin-bottom:1.25rem;}
|
||||
.stat{flex:1;min-width:150px;background:var(--card);border:1px solid var(--line);border-radius:14px;padding:1.1rem 1.3rem;box-shadow:0 6px 18px rgba(20,30,60,.05);}
|
||||
.stat .v{font-size:1.7rem;font-weight:800;color:var(--blue);line-height:1.1;}
|
||||
.stat .k{font-size:.78rem;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-top:.2rem;}
|
||||
.formerr{color:var(--red);font-weight:600;font-size:.88rem;margin:.7rem 0 0;min-height:1em;}
|
||||
.formerr.show{display:flex;align-items:center;gap:.5rem;background:#fee2e2;border:1px solid #fca5a5;border-radius:9px;padding:.6rem .75rem;animation:errShake .35s;}
|
||||
.formerr.show::before{content:"⚠";font-size:1rem;}
|
||||
@keyframes errShake{0%,100%{transform:translateX(0)}20%,60%{transform:translateX(-5px)}40%,80%{transform:translateX(5px)}}
|
||||
.pwwrap{position:relative;} .pwwrap input{padding-right:2.6rem;}
|
||||
.eye{position:absolute;right:.35rem;top:50%;transform:translateY(-50%);background:none;border:none;padding:.3rem;width:auto;color:var(--muted);display:inline-flex;align-items:center;cursor:pointer;}
|
||||
.eye:hover{background:none;color:var(--blue);}
|
||||
.profile{position:relative}
|
||||
.profile .pbtn{display:flex;align-items:center;gap:.4rem;background:rgba(255,255,255,.14);color:#fff;border:1px solid #46598c;border-radius:10px;padding:.45rem .85rem;font-weight:600;font-size:.88rem;cursor:pointer}
|
||||
.profile .pbtn{display:flex;align-items:center;gap:.5rem;background:rgba(255,255,255,.14);color:#fff;border:1px solid #46598c;border-radius:10px;padding:.4rem .85rem .4rem .5rem;font-weight:600;font-size:.88rem;cursor:pointer}
|
||||
.profile .pbtn:hover{background:rgba(255,255,255,.24)}
|
||||
.profile .pmenu{position:absolute;right:0;top:calc(100% + 6px);background:#fff;border:1px solid #e6e9ef;border-radius:10px;box-shadow:0 10px 28px rgba(0,0,0,.18);min-width:190px;overflow:hidden;z-index:5000;display:none}
|
||||
.profile .pbtn .pav{width:28px;height:28px;border-radius:50%;background:var(--brand);color:var(--blue);display:grid;place-items:center;font-weight:800;font-size:.78rem}
|
||||
.profile .pmenu{position:absolute;right:0;top:calc(100% + 6px);background:#fff;border:1px solid #e6e9ef;border-radius:10px;box-shadow:0 10px 28px rgba(0,0,0,.18);min-width:210px;overflow:hidden;z-index:5000;display:none}
|
||||
.profile .pmenu.open{display:block}
|
||||
.profile .pmenu .phead{padding:.7rem .9rem;border-bottom:1px solid #eef1f6}
|
||||
.profile .pmenu .phead .n{font-weight:700;font-size:.9rem}
|
||||
.profile .pmenu .phead .e{color:var(--muted);font-size:.78rem}
|
||||
.profile .pmenu a{display:block;padding:.6rem .9rem;color:#1f2430;text-decoration:none;font-size:.9rem;cursor:pointer}
|
||||
.profile .pmenu a:hover{background:#f1f5f9}
|
||||
.profile .pmenu a.danger{color:#b91c1c;border-top:1px solid #eef1f6}
|
||||
@@ -66,7 +68,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="brandrow"><img src="/logo.png" alt="" style="height:46px;width:auto;max-width:190px;border-radius:8px;object-fit:contain;background:#fff;padding:5px 12px;image-rendering:-webkit-optimize-contrast" onerror="this.replaceWith(Object.assign(document.createElement('div'),{className:'logo',textContent:'B'}))"><div class="brand">BizGaze <span>Support</span> <span style="color:#8ea3cf;font-weight:500;font-size:.85rem">· Console</span></div></div>
|
||||
<div class="brandrow"><img src="/logo.png" alt="" style="height:46px;width:auto;max-width:190px;border-radius:8px;object-fit:contain;background:#fff;padding:5px 12px;image-rendering:-webkit-optimize-contrast" onerror="this.replaceWith(Object.assign(document.createElement('div'),{className:'logo',textContent:'B'}))"><div class="brand">BizGaze <span class="y">Connect</span> <span class="tag">· Dashboard</span></div></div>
|
||||
<div class="row" id="hdrRight"></div>
|
||||
</header>
|
||||
<main id="app"></main>
|
||||
@@ -77,10 +79,19 @@ const EYE_ON='<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke
|
||||
function pwField(id,ph){return '<div class="pwwrap"><input id="'+id+'" type="password" placeholder="'+ph+'"><button type="button" class="eye" data-for="'+id+'" aria-label="Show password"></button></div>';}
|
||||
function wireEyes(){document.querySelectorAll('.eye').forEach(b=>{if(b._w)return;b._w=1;b.innerHTML=EYE_OFF;b.onclick=()=>{const inp=document.getElementById(b.getAttribute('data-for'));if(!inp)return;const show=inp.type==='password';inp.type=show?'text':'password';b.innerHTML=show?EYE_ON:EYE_OFF;};});}
|
||||
function pEsc(s){return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));}
|
||||
function profileHTML(name){return '<div class="profile"><button class="pbtn" id="pbtn">'+pEsc(name)+' <span style="font-size:.65rem">▾</span></button><div class="pmenu" id="pmenu"><a href="/console">Console / Dashboard</a><a id="plogout" class="danger">Logout</a></div></div>';}
|
||||
function initials(name){const p=String(name||'?').trim().split(/\s+/);return ((p[0]||'?')[0]+(p[1]?p[1][0]:'')).toUpperCase();}
|
||||
function profileHTML(u){
|
||||
const display=u.name||u.email;
|
||||
return '<div class="profile"><button class="pbtn" id="pbtn">'
|
||||
+ '<span class="pav">'+pEsc(initials(display))+'</span>'
|
||||
+ pEsc(display)+' <span style="font-size:.65rem">▾</span></button>'
|
||||
+ '<div class="pmenu" id="pmenu">'
|
||||
+ '<div class="phead"><div class="n">'+pEsc(display)+'</div><div class="e">'+pEsc(u.email)+(u.role?' · '+pEsc(u.role):'')+'</div></div>'
|
||||
+ '<a href="/home">Home</a>'
|
||||
+ '<a class="danger" id="plogout">Logout</a>'
|
||||
+ '</div></div>';
|
||||
}
|
||||
function wireProfile(){const btn=document.getElementById('pbtn'),menu=document.getElementById('pmenu');if(!btn)return;btn.onclick=(e)=>{e.stopPropagation();menu.classList.toggle('open');};document.addEventListener('click',()=>menu.classList.remove('open'));const lo=document.getElementById('plogout');if(lo)lo.onclick=async()=>{try{await fetch('/api/logout',{method:'POST'});}catch(_){}location.href='/';};}
|
||||
function makeBrandClickable(){document.querySelectorAll('.brandrow,.wordmark').forEach(el=>{el.style.cursor='pointer';el.addEventListener('click',()=>{location.href='/';});});}
|
||||
makeBrandClickable();
|
||||
const app = document.getElementById('app');
|
||||
const hdrRight = document.getElementById('hdrRight');
|
||||
|
||||
@@ -92,23 +103,20 @@ async function api(path, body, method = 'POST') {
|
||||
if (!r.ok) throw new Error(data.error || 'request failed');
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
function onEnter(ids, fn){ ids.forEach(id => { const el = document.getElementById(id); if (el) el.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); fn(); } }); }); }
|
||||
|
||||
function view(html) { app.innerHTML = html; }
|
||||
|
||||
// ---------- Auth ----------
|
||||
// ---------- Auth (login lives here; on success → home) ----------
|
||||
async function authView() {
|
||||
hdrRight.innerHTML = '';
|
||||
let regOpen = false;
|
||||
try { regOpen = (await api('/api/setup-state', null, 'GET')).registrationOpen; } catch {}
|
||||
view(`
|
||||
<div class="card" style="max-width:420px;margin:3rem auto">
|
||||
<div class="tabs">
|
||||
${regOpen ? `<div class="tabs">
|
||||
<button id="tabLogin" class="active">Sign in</button>
|
||||
${regOpen ? '<button id="tabReg">Register team</button>' : ''}
|
||||
</div>
|
||||
<button id="tabReg">Register team</button>
|
||||
</div>` : ''}
|
||||
<div id="loginForm">
|
||||
<span class="lbl">Email</span>
|
||||
<input id="li_email" placeholder="you@bizgaze.com" type="email">
|
||||
@@ -116,7 +124,7 @@ async function authView() {
|
||||
${pwField("li_pw","password")}
|
||||
<label style="display:flex;align-items:center;gap:.5rem;margin:.7rem 0;color:var(--ink);font-size:.9rem;cursor:pointer"><input type="checkbox" id="li_remember" style="width:18px;height:18px;accent-color:var(--blue);margin:0"> Remember me on this device</label>
|
||||
<button id="li_btn" style="width:100%;margin-top:.5rem">Sign in</button>
|
||||
<p id="li_err" class="muted"></p>
|
||||
<p id="li_err" class="formerr"></p>
|
||||
</div>
|
||||
${regOpen ? `<div id="regForm" class="hidden">
|
||||
<span class="lbl">Team name</span>
|
||||
@@ -126,7 +134,7 @@ async function authView() {
|
||||
<span class="lbl">Password</span>
|
||||
${pwField("rg_pw","min 8 characters")}
|
||||
<button id="rg_btn" style="width:100%;margin-top:1rem">Create team</button>
|
||||
<p id="rg_err" class="muted"></p>
|
||||
<p id="rg_err" class="formerr"></p>
|
||||
</div>` : ''}
|
||||
</div>`);
|
||||
document.getElementById('li_btn').onclick = doLogin;
|
||||
@@ -145,86 +153,56 @@ async function authView() {
|
||||
const rt = document.getElementById('tabReg'); if (rt) rt.classList.toggle('active', !login);
|
||||
}
|
||||
}
|
||||
|
||||
function showErr(id, msg) { const el = document.getElementById(id); el.textContent = msg; el.classList.add('show'); }
|
||||
function clearErr(id) { const el = document.getElementById(id); el.textContent = ''; el.classList.remove('show'); }
|
||||
async function doLogin() {
|
||||
clearErr('li_err');
|
||||
try {
|
||||
const rem = document.getElementById('li_remember');
|
||||
await api('/api/login', { email: li_email.value, password: li_pw.value, remember: rem ? rem.checked : false });
|
||||
location.reload();
|
||||
} catch (e) { li_err.textContent = e.message; }
|
||||
location.href = '/home';
|
||||
} catch (e) {
|
||||
showErr('li_err', /invalid credentials/i.test(e.message) ? 'Incorrect email or password. Please try again.' : e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function doRegister() {
|
||||
clearErr('rg_err');
|
||||
try {
|
||||
await api('/api/register', { email: rg_email.value, password: rg_pw.value, teamName: rg_team.value });
|
||||
await api('/api/login', { email: rg_email.value, password: rg_pw.value });
|
||||
location.reload();
|
||||
} catch (e) { rg_err.textContent = e.message; }
|
||||
location.href = '/home';
|
||||
} catch (e) { showErr('rg_err', e.message); }
|
||||
}
|
||||
|
||||
// ---------- Dashboard ----------
|
||||
let ME = null;
|
||||
let ME = null, IS_ADMIN = false;
|
||||
async function dashboard(me) {
|
||||
ME = me;
|
||||
hdrRight.innerHTML = profileHTML((me.name||me.email)+' · '+me.role); wireProfile();
|
||||
ME = me; IS_ADMIN = (me.role === 'admin');
|
||||
hdrRight.innerHTML = profileHTML(me); wireProfile();
|
||||
view(`
|
||||
<div class="card quick">
|
||||
<div><h2>Start a support session</h2><p>Customer gives you their 6-digit code from the share page.</p></div>
|
||||
<a href="/connect">Open connect page →</a>
|
||||
</div>
|
||||
<div class="card" id="agentsCard">
|
||||
<h2>Agents</h2>
|
||||
<input id="agSearch" class="srch" placeholder="Search agents by name or email">
|
||||
<table id="agents"><thead><tr><th>Email</th><th>Display name</th><th>Role</th><th>Status</th><th style="width:280px"></th></tr></thead><tbody></tbody></table>
|
||||
<div id="agPager" class="pager"></div>
|
||||
<div class="row" style="margin-top:1rem;flex-wrap:wrap">
|
||||
<input id="agEmail" placeholder="agent email" style="max-width:200px">
|
||||
<input id="agName" placeholder="display name (from BizGaze)" style="max-width:210px">
|
||||
<input id="agPw" placeholder="temporary password" style="max-width:170px">
|
||||
<select id="agRole" style="max-width:140px">
|
||||
<option value="technician">technician</option><option value="admin">admin</option><option value="viewer">view-only</option>
|
||||
</select>
|
||||
<button id="agAdd">Add agent</button>
|
||||
</div>
|
||||
<p id="agOut" class="muted"></p>
|
||||
</div>
|
||||
<div class="stats" id="stats"></div>
|
||||
<div class="card">
|
||||
<h2>Session report</h2>
|
||||
<h2>${IS_ADMIN ? 'Connection report — all agents' : 'My connection report'}</h2>
|
||||
<div class="filters">
|
||||
<div class="f"><span class="lbl">Agent</span><select id="fAgent"><option value="">All agents</option></select></div>
|
||||
${IS_ADMIN ? '<div class="f"><span class="lbl">Agent</span><select id="fAgent"><option value="">All agents</option></select></div>' : ''}
|
||||
<div class="f"><span class="lbl">From</span><input id="fFrom" type="date"></div>
|
||||
<div class="f"><span class="lbl">To</span><input id="fTo" type="date"></div>
|
||||
<button id="fApply">Apply</button>
|
||||
<button id="fExcel" class="mini" style="padding:.6rem .9rem">⬇ Excel</button>
|
||||
<button id="fPdf" class="mini" style="padding:.6rem .9rem">⬇ PDF</button>
|
||||
</div>
|
||||
<table id="report"><thead><tr><th>Date</th><th>Start time</th><th>Agent</th><th>Ticket</th><th>Time spent</th><th>Recording / Transcript</th></tr></thead><tbody></tbody></table>
|
||||
${IS_ADMIN ? '<input id="repSearch" class="srch" placeholder="Search by agent or ticket">' : ''}
|
||||
<table id="report"><thead><tr><th>Date</th><th>Start time</th>${IS_ADMIN ? '<th>Agent</th>' : ''}<th>Ticket</th><th>Time spent</th><th>Recording / Transcript</th></tr></thead><tbody></tbody></table>
|
||||
<div id="repPager" class="pager"></div>
|
||||
<p id="repSummary" class="muted" style="margin-top:.6rem"></p>
|
||||
</div>`);
|
||||
|
||||
if (me.role !== 'admin') document.getElementById('agentsCard').style.display = 'none';
|
||||
else {
|
||||
document.getElementById('agAdd').onclick = addAgent;
|
||||
onEnter(['agEmail','agName','agPw'], addAgent);
|
||||
await loadAgents();
|
||||
}
|
||||
document.getElementById('fApply').onclick = loadReport;
|
||||
document.getElementById('fExcel').onclick = exportExcel;
|
||||
document.getElementById('fPdf').onclick = exportPdf;
|
||||
await populateAgentFilter();
|
||||
if (IS_ADMIN) await populateAgentFilter();
|
||||
await loadReport();
|
||||
}
|
||||
|
||||
async function addAgent() {
|
||||
try {
|
||||
const r = await api('/api/users', { email: agEmail.value, name: agName.value, password: agPw.value, role: agRole.value });
|
||||
agOut.textContent = `Agent ${r.email} added. Share the email + temporary password — they sign in at /connect.`;
|
||||
agEmail.value = ''; agName.value = ''; agPw.value = '';
|
||||
loadAgents(); populateAgentFilter();
|
||||
} catch (e) { agOut.textContent = e.message; }
|
||||
}
|
||||
|
||||
const PER_PAGE = 5;
|
||||
function pagerHTML(page, pages, total, fn){
|
||||
if (total <= PER_PAGE) return total ? `<span>${total} total</span>` : '';
|
||||
@@ -232,69 +210,15 @@ function pagerHTML(page, pages, total, fn){
|
||||
+ `<span>Page ${page} of ${pages} · ${total} total</span>`
|
||||
+ `<button ${page>=pages?'disabled':''} onclick="${fn}(${page+1})">Next ›</button>`;
|
||||
}
|
||||
let AGENTS_ALL = [], agentPage = 1, agentSearch = '';
|
||||
function agentRowHTML(u){ return `
|
||||
<tr>
|
||||
<td>${esc(u.email)}</td><td>${esc(u.name || '—')}</td><td>${esc(u.role)}</td>
|
||||
<td><span class="pill ${u.active === 0 ? 'off' : 'on'}">${u.active === 0 ? 'deactivated' : 'active'}</span></td>
|
||||
<td>
|
||||
<button class="mini" onclick="resetPw('${u.id}','${esc(u.email)}')">Reset password</button>
|
||||
<button class="mini" onclick="renameAgent('${u.id}','${esc(u.email)}')">Edit name</button>
|
||||
${u.id === ME.id ? '' : (u.active === 0
|
||||
? `<button class="mini" onclick="manage('${u.id}','activate')">Activate</button>`
|
||||
: `<button class="mini danger" onclick="manage('${u.id}','deactivate')">Deactivate</button>`)
|
||||
}
|
||||
${u.id === ME.id ? '' : `<button class="mini danger" onclick="delAgent('${u.id}','${esc(u.email)}')">Delete</button>`}
|
||||
</td>
|
||||
</tr>`; }
|
||||
async function loadAgents() {
|
||||
AGENTS_ALL = await api('/api/users', null, 'GET');
|
||||
agentPage = 1;
|
||||
const s = document.getElementById('agSearch');
|
||||
if (s && !s._w){ s._w = 1; s.addEventListener('input', () => { agentSearch = s.value.trim().toLowerCase(); agentPage = 1; renderAgents(); }); }
|
||||
renderAgents();
|
||||
}
|
||||
window.agentGo = (p) => { agentPage = p; renderAgents(); };
|
||||
function renderAgents(){
|
||||
const all = agentSearch ? AGENTS_ALL.filter(u => ((u.name||'')+' '+(u.email||'')).toLowerCase().includes(agentSearch)) : AGENTS_ALL;
|
||||
const pages = Math.max(1, Math.ceil(all.length / PER_PAGE));
|
||||
if (agentPage > pages) agentPage = pages;
|
||||
const slice = all.slice((agentPage-1)*PER_PAGE, (agentPage-1)*PER_PAGE + PER_PAGE);
|
||||
document.querySelector('#agents tbody').innerHTML = slice.map(agentRowHTML).join('') || '<tr><td colspan=5 class="muted">No matching agents.</td></tr>';
|
||||
document.getElementById('agPager').innerHTML = pagerHTML(agentPage, pages, all.length, 'agentGo');
|
||||
}
|
||||
|
||||
window.resetPw = async (id, email) => {
|
||||
const pw = prompt(`New password for ${email} (min 8 characters):`);
|
||||
if (!pw) return;
|
||||
try { await api('/api/users/manage', { id, action: 'reset-password', password: pw }); agOut.textContent = `Password reset for ${email}. They were signed out everywhere.`; }
|
||||
catch (e) { agOut.textContent = e.message; }
|
||||
};
|
||||
window.renameAgent = async (id, email) => {
|
||||
const name = prompt(`Display name for ${email} (as in the BizGaze app):`);
|
||||
if (!name) return;
|
||||
try { await api('/api/users/manage', { id, action: 'rename', name }); loadAgents(); }
|
||||
catch (e) { agOut.textContent = e.message; }
|
||||
};
|
||||
window.manage = async (id, action) => {
|
||||
try { await api('/api/users/manage', { id, action }); loadAgents(); }
|
||||
catch (e) { agOut.textContent = e.message; }
|
||||
};
|
||||
window.delAgent = async (id, email) => {
|
||||
if (!confirm(`Delete ${email}? This cannot be undone. (Tip: Deactivate keeps their history.)`)) return;
|
||||
try { await api('/api/users/manage', { id, action: 'delete' }); loadAgents(); populateAgentFilter(); }
|
||||
catch (e) { agOut.textContent = e.message; }
|
||||
};
|
||||
|
||||
// ---------- Session report ----------
|
||||
async function populateAgentFilter() {
|
||||
try {
|
||||
const rows = await api('/api/users', null, 'GET');
|
||||
const sel = document.getElementById('fAgent');
|
||||
const sel = document.getElementById('fAgent'); if (!sel) return;
|
||||
const cur = sel.value;
|
||||
sel.innerHTML = '<option value="">All agents</option>' + rows.map(u => `<option value="${esc(u.email)}">${esc(u.name || u.email)}</option>`).join('');
|
||||
sel.value = cur;
|
||||
} catch { /* non-admins cannot list agents; filter stays "All" */ }
|
||||
} catch { /* non-admins cannot list agents */ }
|
||||
}
|
||||
|
||||
function fmtDuration(ms) {
|
||||
@@ -313,7 +237,7 @@ function reportRowHTML(r){
|
||||
return `<tr>
|
||||
<td>${d.toLocaleDateString()}</td>
|
||||
<td class="muted">${d.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'})}</td>
|
||||
<td>${esc(r.agent_name || r.agent_email || '—')}</td>
|
||||
${IS_ADMIN ? `<td>${esc(r.agent_name || r.agent_email || '—')}</td>` : ''}
|
||||
<td>${esc(r.ticket || 'Direct session')}</td>
|
||||
<td>${r.ended_at ? fmtDuration(dur) : '<span class="pill on">in progress</span>'}</td>
|
||||
<td>${[
|
||||
@@ -324,27 +248,48 @@ function reportRowHTML(r){
|
||||
}
|
||||
async function loadReport() {
|
||||
const q = new URLSearchParams();
|
||||
if (fAgent.value) q.set('agent', fAgent.value);
|
||||
const fa = document.getElementById('fAgent');
|
||||
if (fa && fa.value) q.set('agent', fa.value);
|
||||
if (fFrom.value) q.set('from', fFrom.value);
|
||||
if (fTo.value) q.set('to', fTo.value);
|
||||
REPORT_ROWS = await api('/api/report?' + q.toString(), null, 'GET');
|
||||
reportPage = 1;
|
||||
const s = document.getElementById('repSearch');
|
||||
if (s && !s._w){ s._w = 1; s.addEventListener('input', () => { reportSearch = s.value.trim().toLowerCase(); reportPage = 1; renderReport(); }); }
|
||||
renderStats();
|
||||
renderReport();
|
||||
}
|
||||
window.reportGo = (p) => { reportPage = p; renderReport(); };
|
||||
function filteredRows(){
|
||||
return reportSearch ? REPORT_ROWS.filter(r => ((r.agent_name||'')+' '+(r.agent_email||'')+' '+(r.ticket||'')).toLowerCase().includes(reportSearch)) : REPORT_ROWS;
|
||||
}
|
||||
function renderStats(){
|
||||
const el = document.getElementById('stats'); if (!el) return;
|
||||
const rows = REPORT_ROWS;
|
||||
const total = rows.length;
|
||||
const totalMs = rows.reduce((a, r) => a + (r.ended_at ? r.ended_at - r.started_at : 0), 0);
|
||||
const recs = rows.filter(r => r.recording).length;
|
||||
const cards = [
|
||||
{ v: total, k: IS_ADMIN ? 'Total sessions' : 'My sessions' },
|
||||
{ v: fmtDuration(totalMs), k: 'Time spent' },
|
||||
{ v: recs, k: 'Recorded' },
|
||||
];
|
||||
el.innerHTML = cards.map(c => `<div class="stat"><div class="v">${esc(String(c.v))}</div><div class="k">${esc(c.k)}</div></div>`).join('');
|
||||
}
|
||||
function renderReport(){
|
||||
const all = reportSearch ? REPORT_ROWS.filter(r => ((r.agent_name||'')+' '+(r.agent_email||'')+' '+(r.ticket||'')).toLowerCase().includes(reportSearch)) : REPORT_ROWS;
|
||||
const all = filteredRows();
|
||||
const pages = Math.max(1, Math.ceil(all.length / PER_PAGE));
|
||||
if (reportPage > pages) reportPage = pages;
|
||||
const slice = all.slice((reportPage-1)*PER_PAGE, (reportPage-1)*PER_PAGE + PER_PAGE);
|
||||
document.querySelector('#report tbody').innerHTML = slice.map(reportRowHTML).join('') || '<tr><td colspan=6 class="muted">No sessions match.</td></tr>';
|
||||
const cols = IS_ADMIN ? 6 : 5;
|
||||
document.querySelector('#report tbody').innerHTML = slice.map(reportRowHTML).join('') || `<tr><td colspan=${cols} class="muted">No sessions match.</td></tr>`;
|
||||
document.getElementById('repPager').innerHTML = pagerHTML(reportPage, pages, all.length, 'reportGo');
|
||||
const total = all.reduce((a, r) => a + (r.ended_at ? r.ended_at - r.started_at : 0), 0);
|
||||
repSummary.textContent = all.length ? `${all.length} session(s) · total time ${fmtDuration(total)}` : '';
|
||||
}
|
||||
|
||||
function reportData() {
|
||||
return REPORT_ROWS.map((r) => {
|
||||
return filteredRows().map((r) => {
|
||||
const d = new Date(r.started_at);
|
||||
return {
|
||||
date: d.toLocaleDateString(), start: d.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}),
|
||||
@@ -353,27 +298,27 @@ function reportData() {
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function exportExcel() {
|
||||
const rows = reportData();
|
||||
if (!rows.length) { repSummary.textContent = 'Nothing to export for this period.'; return; }
|
||||
const head = ['Date','Start time','Agent','Ticket','Time spent'];
|
||||
const head = ['Date','Start time'].concat(IS_ADMIN ? ['Agent'] : []).concat(['Ticket','Time spent']);
|
||||
const csvCell = (v) => '"' + String(v).replace(/"/g, '""') + '"';
|
||||
const csv = '\ufeff' + [head, ...rows.map(r => [r.date, r.start, r.agent, r.ticket, r.spent])]
|
||||
const out = '' + [head, ...rows.map(r => [r.date, r.start].concat(IS_ADMIN ? [r.agent] : []).concat([r.ticket, r.spent]))]
|
||||
.map(line => line.map(csvCell).join(',')).join('\r\n');
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(new Blob([csv], { type: 'text/csv;charset=utf-8' }));
|
||||
a.download = 'session-report.csv';
|
||||
a.href = URL.createObjectURL(new Blob([out], { type: 'text/csv;charset=utf-8' }));
|
||||
a.download = 'connection-report.csv';
|
||||
a.click(); URL.revokeObjectURL(a.href);
|
||||
}
|
||||
|
||||
function exportPdf() {
|
||||
const rows = reportData();
|
||||
if (!rows.length) { repSummary.textContent = 'Nothing to export for this period.'; return; }
|
||||
const period = (fFrom.value || 'start') + ' to ' + (fTo.value || 'today');
|
||||
const agentSel = fAgent.value || 'All agents';
|
||||
const fa = document.getElementById('fAgent');
|
||||
const agentSel = IS_ADMIN ? (fa && fa.value || 'All agents') : (ME.name || ME.email);
|
||||
const w = window.open('', '_blank');
|
||||
w.document.write('<html><head><title>Session report</title><style>' +
|
||||
const headCells = ['Date','Start time'].concat(IS_ADMIN ? ['Agent'] : []).concat(['Ticket','Time spent']);
|
||||
w.document.write('<html><head><title>Connection report</title><style>' +
|
||||
'body{font-family:Segoe UI,Arial,sans-serif;color:#1f2430;margin:32px}' +
|
||||
'h1{font-size:18px;color:#1F3B73;border-bottom:3px solid #FFC708;padding-bottom:6px}' +
|
||||
'.meta{color:#6b7280;font-size:12px;margin-bottom:14px}' +
|
||||
@@ -381,10 +326,10 @@ function exportPdf() {
|
||||
'th{background:#1F3B73;color:#fff;text-align:left;padding:6px 8px}' +
|
||||
'td{padding:6px 8px;border-bottom:1px solid #e6e9ef}' +
|
||||
'</style></head><body>' +
|
||||
'<h1>BizGaze Support — Session report</h1>' +
|
||||
'<div class="meta">Agent: ' + esc(agentSel) + ' · Period: ' + esc(period) + ' · Generated ' + new Date().toLocaleString() + '</div>' +
|
||||
'<table><tr><th>Date</th><th>Start time</th><th>Agent</th><th>Ticket</th><th>Time spent</th></tr>' +
|
||||
rows.map(r => '<tr><td>' + [r.date, r.start, esc(r.agent), esc(r.ticket), r.spent].join('</td><td>') + '</td></tr>').join('') +
|
||||
'<h1>BizGaze Connect — Connection report</h1>' +
|
||||
'<div class="meta">' + esc(IS_ADMIN ? 'Agent: ' + agentSel : 'Agent: ' + agentSel) + ' · Period: ' + esc(period) + ' · Generated ' + new Date().toLocaleString() + '</div>' +
|
||||
'<table><tr>' + headCells.map(h => '<th>' + esc(h) + '</th>').join('') + '</tr>' +
|
||||
rows.map(r => '<tr><td>' + [r.date, r.start].concat(IS_ADMIN ? [esc(r.agent)] : []).concat([esc(r.ticket), r.spent]).join('</td><td>') + '</td></tr>').join('') +
|
||||
'</table><div class="meta" style="margin-top:12px">' + esc(repSummary.textContent) + '</div></body></html>');
|
||||
w.document.close();
|
||||
w.onload = () => { w.print(); };
|
||||
@@ -393,9 +338,10 @@ function exportPdf() {
|
||||
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); }
|
||||
|
||||
// ---------- Boot ----------
|
||||
// Login lives on /home — send logged-out visitors there.
|
||||
(async function () {
|
||||
try { const me = await api('/api/me', null, 'GET'); dashboard(me); }
|
||||
catch { authView(); }
|
||||
catch { location.href = '/home'; }
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
@@ -0,0 +1,277 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>BizGaze Connect — Home</title>
|
||||
<style>
|
||||
:root{ --brand:#FFC708; --brand-d:#E0AC00; --blue:#1F3B73; --blue-d:#16294f; --blue-soft:#EAF0FB; --ink:#1f2430; --muted:#6b7280; --bg:#f6f8fb; --card:#fff; --line:#e6e9ef; --green:#16a34a; --red:#b91c1c; }
|
||||
*{box-sizing:border-box;}
|
||||
html,body{height:100%;}
|
||||
body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--ink);margin:0;display:flex;flex-direction:column;height:100vh;overflow:hidden;}
|
||||
|
||||
/* ---- Top bar (matches console.html) ---- */
|
||||
header{background:var(--blue);padding:.75rem 1.5rem;display:flex;justify-content:space-between;align-items:center;flex:0 0 auto;}
|
||||
.brandrow{display:flex;align-items:center;gap:.6rem;cursor:pointer;}
|
||||
.logo{width:30px;height:30px;border-radius:8px;background:var(--brand);display:grid;place-items:center;font-weight:800;color:var(--blue);}
|
||||
.brand{font-weight:700;color:#fff;font-size:1.05rem;} .brand span.y{color:var(--brand);font-weight:700;}
|
||||
.brand span.tag{color:#8ea3cf;font-weight:500;font-size:.85rem;}
|
||||
|
||||
/* ---- Profile dropdown (from console.html) ---- */
|
||||
.profile{position:relative}
|
||||
.profile .pbtn{display:flex;align-items:center;gap:.5rem;background:rgba(255,255,255,.14);color:#fff;border:1px solid #46598c;border-radius:10px;padding:.4rem .85rem .4rem .5rem;font-weight:600;font-size:.88rem;cursor:pointer}
|
||||
.profile .pbtn:hover{background:rgba(255,255,255,.24)}
|
||||
.profile .pbtn .pav{width:28px;height:28px;border-radius:50%;background:var(--brand);color:var(--blue);display:grid;place-items:center;font-weight:800;font-size:.78rem}
|
||||
.profile .pmenu{position:absolute;right:0;top:calc(100% + 6px);background:#fff;border:1px solid #e6e9ef;border-radius:10px;box-shadow:0 10px 28px rgba(0,0,0,.18);min-width:210px;overflow:hidden;z-index:5000;display:none}
|
||||
.profile .pmenu.open{display:block}
|
||||
.profile .pmenu .phead{padding:.7rem .9rem;border-bottom:1px solid #eef1f6}
|
||||
.profile .pmenu .phead .n{font-weight:700;font-size:.9rem}
|
||||
.profile .pmenu .phead .e{color:var(--muted);font-size:.78rem}
|
||||
.profile .pmenu a{display:block;padding:.6rem .9rem;color:#1f2430;text-decoration:none;font-size:.9rem;cursor:pointer}
|
||||
.profile .pmenu a:hover{background:#f1f5f9}
|
||||
.profile .pmenu a.danger{color:#b91c1c;border-top:1px solid #eef1f6}
|
||||
|
||||
/* ---- Shell ---- */
|
||||
.shell{flex:1 1 auto;display:flex;min-height:0;}
|
||||
|
||||
/* ---- Sidebar ---- */
|
||||
.sidebar{width:320px;flex:0 0 320px;background:var(--card);border-right:1px solid var(--line);display:flex;flex-direction:column;min-height:0;}
|
||||
.side-head{padding:1rem 1rem .75rem;border-bottom:1px solid var(--line);}
|
||||
.side-title{display:flex;align-items:center;justify-content:space-between;margin-bottom:.7rem;}
|
||||
.side-title h2{font-size:.95rem;margin:0;color:var(--blue);}
|
||||
.newchat{width:30px;height:30px;border-radius:9px;border:none;background:var(--blue-soft);color:var(--blue);font-size:1.2rem;line-height:1;cursor:pointer;font-weight:700;display:grid;place-items:center;padding:0;}
|
||||
.newchat:hover{background:#dbe6fb;}
|
||||
.search{position:relative;}
|
||||
.search svg{position:absolute;left:.65rem;top:50%;transform:translateY(-50%);color:var(--muted);}
|
||||
.search input{width:100%;padding:.55rem .7rem .55rem 2.1rem;border-radius:10px;border:2px solid var(--line);background:#fbfcfe;color:var(--ink);font-size:.9rem;}
|
||||
.search input:focus{outline:none;border-color:var(--brand);}
|
||||
|
||||
.chatlist{overflow-y:auto;flex:1 1 auto;padding:.4rem;}
|
||||
.chat-row{display:flex;gap:.7rem;align-items:center;padding:.6rem .65rem;border-radius:12px;cursor:pointer;position:relative;}
|
||||
.chat-row:hover{background:#f3f6fb;}
|
||||
.chat-row.active{background:var(--blue-soft);}
|
||||
.chat-row.active::before{content:"";position:absolute;left:0;top:.7rem;bottom:.7rem;width:3px;border-radius:3px;background:var(--blue);}
|
||||
.avatar{width:42px;height:42px;flex:0 0 42px;border-radius:50%;display:grid;place-items:center;color:#fff;font-weight:700;font-size:.92rem;position:relative;}
|
||||
.avatar .dot{position:absolute;right:-1px;bottom:-1px;width:11px;height:11px;border-radius:50%;border:2px solid #fff;background:#cbd2dd;}
|
||||
.avatar .dot.on{background:var(--green);}
|
||||
.chat-main{flex:1 1 auto;min-width:0;}
|
||||
.chat-top{display:flex;justify-content:space-between;align-items:baseline;gap:.5rem;}
|
||||
.chat-name{font-weight:600;font-size:.92rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
||||
.chat-time{color:var(--muted);font-size:.72rem;flex:0 0 auto;}
|
||||
.chat-bottom{display:flex;justify-content:space-between;align-items:center;gap:.5rem;margin-top:.15rem;}
|
||||
.chat-prev{color:var(--muted);font-size:.82rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1 1 auto;}
|
||||
.chat-row.unread .chat-prev{color:var(--ink);font-weight:500;}
|
||||
.chat-row.unread .chat-name{font-weight:700;}
|
||||
.badge{flex:0 0 auto;background:var(--blue);color:#fff;font-size:.7rem;font-weight:700;min-width:19px;height:19px;border-radius:99px;padding:0 .35rem;display:grid;place-items:center;}
|
||||
.no-results{padding:2rem 1rem;text-align:center;color:var(--muted);font-size:.85rem;}
|
||||
|
||||
/* ---- Main content ---- */
|
||||
.content{flex:1 1 auto;display:flex;flex-direction:column;min-width:0;min-height:0;}
|
||||
.tabs{display:flex;gap:.4rem;padding:1rem 1.5rem 0;border-bottom:1px solid var(--line);background:var(--card);}
|
||||
.tabs button{background:transparent;color:var(--muted);font-weight:600;font-size:.92rem;border:none;border-bottom:3px solid transparent;padding:.6rem .9rem .8rem;cursor:pointer;display:flex;align-items:center;gap:.45rem;border-radius:8px 8px 0 0;}
|
||||
.tabs button:hover{color:var(--blue);background:#f6f8fb;}
|
||||
.tabs button.active{color:var(--blue);border-bottom-color:var(--brand);}
|
||||
.panel-wrap{flex:1 1 auto;overflow-y:auto;padding:2rem 1.5rem;display:flex;}
|
||||
.panel{display:none;margin:auto;width:100%;max-width:560px;}
|
||||
.panel.active{display:block;}
|
||||
|
||||
.card{background:var(--card);border:1px solid var(--line);border-radius:16px;padding:2.2rem;box-shadow:0 6px 18px rgba(20,30,60,.05);text-align:center;}
|
||||
.feat-icon{width:72px;height:72px;border-radius:20px;display:grid;place-items:center;margin:0 auto 1.2rem;}
|
||||
.feat-icon.blue{background:var(--blue-soft);color:var(--blue);}
|
||||
.feat-icon.yellow{background:#fff6d8;color:var(--brand-d);}
|
||||
.card h1{font-size:1.45rem;margin:0 0 .5rem;color:var(--blue);}
|
||||
.card p{color:var(--muted);font-size:.95rem;line-height:1.55;margin:0 auto 1.6rem;max-width:400px;}
|
||||
.btn{display:inline-flex;align-items:center;gap:.5rem;text-decoration:none;padding:.8rem 1.6rem;background:var(--brand);color:var(--ink);border:none;border-radius:11px;font-weight:700;font-size:.95rem;cursor:pointer;}
|
||||
.btn:hover{background:var(--brand-d);}
|
||||
.pill-soon{display:inline-block;background:#fff6d8;color:var(--brand-d);font-size:.74rem;font-weight:700;padding:.25rem .7rem;border-radius:99px;letter-spacing:.03em;margin-bottom:1.2rem;}
|
||||
.hint{margin-top:1.4rem;font-size:.8rem;color:var(--muted);}
|
||||
|
||||
@media (max-width:760px){
|
||||
.sidebar{width:108px;flex:0 0 108px;}
|
||||
.side-title h2,.search,.chat-main{display:none;}
|
||||
.chat-row{justify-content:center;}
|
||||
.side-head{padding:.8rem .5rem;}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="brandrow" id="brandrow">
|
||||
<img src="/logo.png" alt="" style="height:46px;width:auto;max-width:190px;border-radius:8px;object-fit:contain;background:#fff;padding:5px 12px;image-rendering:-webkit-optimize-contrast" onerror="this.replaceWith(Object.assign(document.createElement('div'),{className:'logo',textContent:'B'}))">
|
||||
<div class="brand">BizGaze <span class="y">Connect</span> <span class="tag">· Home</span></div>
|
||||
</div>
|
||||
<div id="hdrRight"></div>
|
||||
</header>
|
||||
|
||||
<div class="shell">
|
||||
<!-- ---------- Sidebar ---------- -->
|
||||
<aside class="sidebar">
|
||||
<div class="side-head">
|
||||
<div class="side-title">
|
||||
<h2>Chats</h2>
|
||||
<button class="newchat" title="New chat" aria-label="New chat">+</button>
|
||||
</div>
|
||||
<div class="search">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input id="chatSearch" placeholder="Search chats" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
<div class="chatlist" id="chatlist"></div>
|
||||
</aside>
|
||||
|
||||
<!-- ---------- Main ---------- -->
|
||||
<section class="content">
|
||||
<div class="tabs">
|
||||
<button data-tab="meeting" class="active">
|
||||
<svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg>
|
||||
Meeting
|
||||
</button>
|
||||
<button data-tab="share">
|
||||
<svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
|
||||
Share Screen
|
||||
</button>
|
||||
<button data-tab="connect">
|
||||
<svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/></svg>
|
||||
Connect Screen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="panel-wrap">
|
||||
<!-- Meeting -->
|
||||
<div class="panel active" data-panel="meeting">
|
||||
<div class="card">
|
||||
<div class="feat-icon yellow">
|
||||
<svg viewBox="0 0 24 24" width="34" height="34" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg>
|
||||
</div>
|
||||
<span class="pill-soon">COMING SOON</span>
|
||||
<h1>Meetings are on the way</h1>
|
||||
<p>Soon you'll be able to host multi-party video meetings with your BizGaze team and customers — right here, no install needed. We're putting on the finishing touches.</p>
|
||||
<button class="btn" id="notifyBtn">🔔 Notify me when it's ready</button>
|
||||
<div class="hint">In the meantime, use <b>Share Screen</b> or <b>Connect Screen</b> to start a session.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Share Screen -->
|
||||
<div class="panel" data-panel="share">
|
||||
<div class="card">
|
||||
<div class="feat-icon blue">
|
||||
<svg viewBox="0 0 24 24" width="34" height="34" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
|
||||
</div>
|
||||
<h1>Share your screen</h1>
|
||||
<p>Let a teammate or customer see your screen instantly. You'll get a 6-digit code to share — they enter it to connect. No download, works right in the browser.</p>
|
||||
<a class="btn" href="/share">Start sharing →</a>
|
||||
<div class="hint">Desktop browsers only — phones can't share their screen yet.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Connect Screen -->
|
||||
<div class="panel" data-panel="connect">
|
||||
<div class="card">
|
||||
<div class="feat-icon blue">
|
||||
<svg viewBox="0 0 24 24" width="34" height="34" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/></svg>
|
||||
</div>
|
||||
<h1>Connect to a screen</h1>
|
||||
<p>Helping someone out? Enter the 6-digit code they give you to view their screen and provide live support — with two-way voice and chat built in.</p>
|
||||
<a class="btn" href="/connect">Open connect page →</a>
|
||||
<div class="hint">The other person taps <b>Allow</b> before you can see anything.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ---------- Helpers (reused patterns from console.html) ----------
|
||||
function pEsc(s){return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));}
|
||||
function initials(name){return name.trim().split(/\s+/).slice(0,2).map(w=>w[0]).join('').toUpperCase();}
|
||||
|
||||
// Stable avatar color from a name
|
||||
const AV_COLORS=['#1F3B73','#2563eb','#0e7490','#7c3aed','#be185d','#b45309','#15803d','#9d174d'];
|
||||
function avColor(name){let h=0;for(const c of name)h=(h*31+c.charCodeAt(0))>>>0;return AV_COLORS[h%AV_COLORS.length];}
|
||||
|
||||
// Profile dropdown (mirrors profileHTML()/wireProfile() from console.html)
|
||||
const SAMPLE_USER={name:'Sravan Mareddy',email:'sravanm@bizgaze.com',role:'admin'};
|
||||
function profileHTML(u){
|
||||
return '<div class="profile"><button class="pbtn" id="pbtn">'
|
||||
+ '<span class="pav">'+pEsc(initials(u.name))+'</span>'
|
||||
+ pEsc(u.name)+' <span style="font-size:.65rem">▾</span></button>'
|
||||
+ '<div class="pmenu" id="pmenu">'
|
||||
+ '<div class="phead"><div class="n">'+pEsc(u.name)+'</div><div class="e">'+pEsc(u.email)+' · '+pEsc(u.role)+'</div></div>'
|
||||
+ '<a href="/console">Console / Dashboard</a>'
|
||||
+ '<a href="#">Settings</a>'
|
||||
+ '<a class="danger" id="plogout">Logout</a>'
|
||||
+ '</div></div>';
|
||||
}
|
||||
function wireProfile(){
|
||||
const btn=document.getElementById('pbtn'),menu=document.getElementById('pmenu');
|
||||
if(!btn)return;
|
||||
btn.onclick=(e)=>{e.stopPropagation();menu.classList.toggle('open');};
|
||||
document.addEventListener('click',()=>menu.classList.remove('open'));
|
||||
const lo=document.getElementById('plogout');
|
||||
if(lo)lo.onclick=(e)=>{e.preventDefault();alert('Mockup — logout would sign you out and return to /.');};
|
||||
}
|
||||
document.getElementById('hdrRight').innerHTML=profileHTML(SAMPLE_USER);
|
||||
wireProfile();
|
||||
document.getElementById('brandrow').onclick=()=>{location.href='/';};
|
||||
|
||||
// ---------- Mock chat data ----------
|
||||
const CHATS=[
|
||||
{name:'Anwi Systems', msg:"Perfect, the screen share worked great. Thanks!", time:'9:42 AM', unread:0, online:true, active:true},
|
||||
{name:'Priya Sharma', msg:"Can you connect to my screen at 3pm?", time:'9:15 AM', unread:2, online:true},
|
||||
{name:'GAPL Group', msg:"You: I've shared the 6-digit code with you", time:'Yesterday', unread:0, online:false},
|
||||
{name:'Battery Doctors', msg:"The invoice module is throwing an error again", time:'Yesterday', unread:5, online:true},
|
||||
{name:'Ramesh Marketing', msg:"You: Let me know once you're at your desk", time:'Mon', unread:0, online:false},
|
||||
{name:'STC Support', msg:"Typing…", time:'Mon', unread:1, online:true},
|
||||
{name:'Samruddhi Traders',msg:"Thanks for the help earlier 👍", time:'Sun', unread:0, online:false},
|
||||
{name:'DMS 3.0 Team', msg:"You: Closing the ticket, all resolved", time:'Fri', unread:0, online:false},
|
||||
];
|
||||
|
||||
const listEl=document.getElementById('chatlist');
|
||||
function chatRowHTML(c,i){
|
||||
const cls=['chat-row'];
|
||||
if(c.active)cls.push('active');
|
||||
if(c.unread>0)cls.push('unread');
|
||||
return '<div class="'+cls.join(' ')+'" data-i="'+i+'">'
|
||||
+ '<div class="avatar" style="background:'+avColor(c.name)+'">'+pEsc(initials(c.name))
|
||||
+ '<span class="dot'+(c.online?' on':'')+'"></span></div>'
|
||||
+ '<div class="chat-main">'
|
||||
+ '<div class="chat-top"><span class="chat-name">'+pEsc(c.name)+'</span><span class="chat-time">'+pEsc(c.time)+'</span></div>'
|
||||
+ '<div class="chat-bottom"><span class="chat-prev">'+pEsc(c.msg)+'</span>'
|
||||
+ (c.unread>0?'<span class="badge">'+c.unread+'</span>':'')+'</div>'
|
||||
+ '</div></div>';
|
||||
}
|
||||
function renderChats(filter){
|
||||
const q=(filter||'').trim().toLowerCase();
|
||||
const rows=CHATS.map((c,i)=>({c,i})).filter(({c})=>!q||c.name.toLowerCase().includes(q)||c.msg.toLowerCase().includes(q));
|
||||
listEl.innerHTML = rows.length
|
||||
? rows.map(({c,i})=>chatRowHTML(c,i)).join('')
|
||||
: '<div class="no-results">No chats match “'+pEsc(filter)+'”.</div>';
|
||||
listEl.querySelectorAll('.chat-row').forEach(row=>{
|
||||
row.onclick=()=>{
|
||||
CHATS.forEach(c=>c.active=false);
|
||||
CHATS[+row.dataset.i].active=true;
|
||||
CHATS[+row.dataset.i].unread=0;
|
||||
renderChats(document.getElementById('chatSearch').value);
|
||||
};
|
||||
});
|
||||
}
|
||||
renderChats('');
|
||||
document.getElementById('chatSearch').addEventListener('input',e=>renderChats(e.target.value));
|
||||
|
||||
// ---------- Tab switching ----------
|
||||
const tabBtns=document.querySelectorAll('.tabs button');
|
||||
const panels=document.querySelectorAll('.panel');
|
||||
tabBtns.forEach(btn=>{
|
||||
btn.onclick=()=>{
|
||||
const tab=btn.dataset.tab;
|
||||
tabBtns.forEach(b=>b.classList.toggle('active',b===btn));
|
||||
panels.forEach(p=>p.classList.toggle('active',p.dataset.panel===tab));
|
||||
};
|
||||
});
|
||||
|
||||
// Mockup-only stubs
|
||||
document.querySelector('.newchat').onclick=()=>alert('Mockup — “New chat” would open the contact picker.');
|
||||
document.getElementById('notifyBtn').onclick=()=>alert("Thanks! We'll let you know when Meetings launches.");
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,478 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>BizGaze Connect</title>
|
||||
<style>
|
||||
:root{ --brand:#FFC708; --brand-d:#E0AC00; --blue:#1F3B73; --blue-d:#16294f; --blue-soft:#EAF0FB; --ink:#1f2430; --muted:#6b7280; --bg:#f6f8fb; --card:#fff; --line:#e6e9ef; --green:#16a34a; --red:#b91c1c; }
|
||||
*{box-sizing:border-box;}
|
||||
html,body{height:100%;}
|
||||
body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--ink);margin:0;display:flex;flex-direction:column;height:100vh;overflow:hidden;}
|
||||
|
||||
/* ---- Top bar ---- */
|
||||
header{background:var(--blue);padding:.7rem 1.4rem;display:flex;justify-content:space-between;align-items:center;flex:0 0 auto;}
|
||||
.brandrow{display:flex;align-items:center;gap:.6rem;}
|
||||
.logo{width:30px;height:30px;border-radius:8px;background:var(--brand);display:grid;place-items:center;font-weight:800;color:var(--blue);}
|
||||
.brand{font-weight:700;color:#fff;font-size:1.05rem;} .brand span.y{color:var(--brand);font-weight:700;}
|
||||
|
||||
/* ---- Profile dropdown (from console.html) ---- */
|
||||
.profile{position:relative}
|
||||
.profile .pbtn{display:flex;align-items:center;gap:.5rem;background:rgba(255,255,255,.14);color:#fff;border:1px solid #46598c;border-radius:10px;padding:.4rem .85rem .4rem .5rem;font-weight:600;font-size:.88rem;cursor:pointer}
|
||||
.profile .pbtn:hover{background:rgba(255,255,255,.24)}
|
||||
.profile .pbtn .pav{width:28px;height:28px;border-radius:50%;background:var(--brand);color:var(--blue);display:grid;place-items:center;font-weight:800;font-size:.78rem}
|
||||
.profile .pmenu{position:absolute;right:0;top:calc(100% + 6px);background:#fff;border:1px solid #e6e9ef;border-radius:10px;box-shadow:0 10px 28px rgba(0,0,0,.18);min-width:210px;overflow:hidden;z-index:5000;display:none}
|
||||
.profile .pmenu.open{display:block}
|
||||
.profile .pmenu .phead{padding:.7rem .9rem;border-bottom:1px solid #eef1f6}
|
||||
.profile .pmenu .phead .n{font-weight:700;font-size:.9rem}
|
||||
.profile .pmenu .phead .e{color:var(--muted);font-size:.78rem}
|
||||
.profile .pmenu a{display:block;padding:.6rem .9rem;color:#1f2430;text-decoration:none;font-size:.9rem;cursor:pointer}
|
||||
.profile .pmenu a:hover{background:#f1f5f9}
|
||||
.profile .pmenu a.danger{color:#b91c1c;border-top:1px solid #eef1f6}
|
||||
|
||||
/* ---- Shell ---- */
|
||||
.shell{flex:1 1 auto;display:flex;min-height:0;}
|
||||
|
||||
/* ---- Icon rail ---- */
|
||||
.rail{width:74px;flex:0 0 74px;background:var(--card);border-right:1px solid var(--line);display:flex;flex-direction:column;align-items:center;padding:.8rem 0;gap:.4rem;}
|
||||
.railbtn{position:relative;width:50px;height:50px;border:none;background:transparent;border-radius:14px;color:var(--muted);cursor:pointer;display:grid;place-items:center;transition:background .12s,color .12s;}
|
||||
.railbtn:hover{background:var(--blue-soft);color:var(--blue);}
|
||||
.railbtn.active{background:var(--blue);color:#fff;}
|
||||
.railbtn .rdot{position:absolute;top:8px;right:8px;min-width:16px;height:16px;border-radius:99px;background:var(--brand);color:var(--blue);font-size:.62rem;font-weight:800;display:grid;place-items:center;padding:0 .2rem;border:2px solid var(--card);}
|
||||
.railbtn.active .rdot{border-color:var(--blue);}
|
||||
.railbtn .livedot{position:absolute;top:8px;right:8px;width:11px;height:11px;border-radius:50%;background:var(--green);border:2px solid var(--card);display:none;}
|
||||
.railbtn.active .livedot{border-color:var(--blue);}
|
||||
.railbtn.live .livedot{display:block;animation:livePulse 1.4s infinite;}
|
||||
@keyframes livePulse{0%,100%{opacity:1}50%{opacity:.3}}
|
||||
.railbtn .rlabel{font-size:.6rem;margin-top:0;}
|
||||
/* tooltip */
|
||||
.railbtn::after{content:attr(data-tip);position:absolute;left:calc(100% + 12px);top:50%;transform:translateY(-50%);background:var(--blue-d);color:#fff;padding:.35rem .6rem;border-radius:8px;font-size:.78rem;font-weight:600;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity .14s;z-index:200;box-shadow:0 6px 16px rgba(0,0,0,.25);}
|
||||
.railbtn::before{content:"";position:absolute;left:calc(100% + 6px);top:50%;transform:translateY(-50%);border:6px solid transparent;border-right-color:var(--blue-d);opacity:0;pointer-events:none;transition:opacity .14s;z-index:200;}
|
||||
.railbtn:hover::after,.railbtn:hover::before{opacity:1;}
|
||||
.rail-spacer{flex:1 1 auto;}
|
||||
.caption{font-size:.58rem;color:var(--muted);text-align:center;line-height:1.2;}
|
||||
|
||||
/* ---- Chat list column ---- */
|
||||
.chatcol{width:312px;flex:0 0 312px;background:var(--card);border-right:1px solid var(--line);display:flex;flex-direction:column;min-height:0;}
|
||||
.chatcol.hidden{display:none;}
|
||||
.side-head{padding:1rem 1rem .75rem;border-bottom:1px solid var(--line);}
|
||||
.side-title{display:flex;align-items:center;justify-content:space-between;margin-bottom:.7rem;}
|
||||
.side-title h2{font-size:.95rem;margin:0;color:var(--blue);}
|
||||
.newchat{width:30px;height:30px;border-radius:9px;border:none;background:var(--blue-soft);color:var(--blue);font-size:1.2rem;line-height:1;cursor:pointer;font-weight:700;display:grid;place-items:center;padding:0;}
|
||||
.newchat:hover{background:#dbe6fb;}
|
||||
.search{position:relative;}
|
||||
.search svg{position:absolute;left:.65rem;top:50%;transform:translateY(-50%);color:var(--muted);}
|
||||
.search input{width:100%;padding:.55rem .7rem .55rem 2.1rem;border-radius:10px;border:2px solid var(--line);background:#fbfcfe;color:var(--ink);font-size:.9rem;}
|
||||
.search input:focus{outline:none;border-color:var(--brand);}
|
||||
|
||||
.chatlist{overflow-y:auto;flex:1 1 auto;padding:.4rem;}
|
||||
.chat-row{display:flex;gap:.7rem;align-items:center;padding:.6rem .65rem;border-radius:12px;cursor:pointer;position:relative;}
|
||||
.chat-row:hover{background:#f3f6fb;}
|
||||
.chat-row.active{background:var(--blue-soft);}
|
||||
.chat-row.active::before{content:"";position:absolute;left:0;top:.7rem;bottom:.7rem;width:3px;border-radius:3px;background:var(--blue);}
|
||||
.avatar{width:42px;height:42px;flex:0 0 42px;border-radius:50%;display:grid;place-items:center;color:#fff;font-weight:700;font-size:.92rem;position:relative;}
|
||||
.avatar .dot{position:absolute;right:-1px;bottom:-1px;width:11px;height:11px;border-radius:50%;border:2px solid #fff;background:#cbd2dd;}
|
||||
.avatar .dot.on{background:var(--green);}
|
||||
.chat-main{flex:1 1 auto;min-width:0;}
|
||||
.chat-top{display:flex;justify-content:space-between;align-items:baseline;gap:.5rem;}
|
||||
.chat-name{font-weight:600;font-size:.92rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
||||
.chat-time{color:var(--muted);font-size:.72rem;flex:0 0 auto;}
|
||||
.chat-bottom{display:flex;justify-content:space-between;align-items:center;gap:.5rem;margin-top:.15rem;}
|
||||
.chat-prev{color:var(--muted);font-size:.82rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1 1 auto;}
|
||||
.chat-row.unread .chat-prev{color:var(--ink);font-weight:500;}
|
||||
.chat-row.unread .chat-name{font-weight:700;}
|
||||
.badge{flex:0 0 auto;background:var(--blue);color:#fff;font-size:.7rem;font-weight:700;min-width:19px;height:19px;border-radius:99px;padding:0 .35rem;display:grid;place-items:center;}
|
||||
.no-results{padding:2rem 1rem;text-align:center;color:var(--muted);font-size:.85rem;}
|
||||
.demo-note{padding:.5rem 1rem;border-top:1px solid var(--line);color:var(--muted);font-size:.72rem;text-align:center;background:#fbfcfe;}
|
||||
|
||||
/* ---- Main content ---- */
|
||||
.content{flex:1 1 auto;position:relative;min-width:0;min-height:0;background:var(--bg);}
|
||||
.panel{position:absolute;inset:0;display:none;}
|
||||
.panel.active{display:flex;}
|
||||
.panel.center{align-items:center;justify-content:center;padding:2rem;overflow-y:auto;}
|
||||
.panel iframe{width:100%;height:100%;border:0;display:block;background:var(--bg);}
|
||||
|
||||
/* welcome + feature cards */
|
||||
.welcome{text-align:center;max-width:560px;}
|
||||
.welcome .wave{font-size:3rem;line-height:1;margin-bottom:.4rem;}
|
||||
.welcome h1{font-size:1.8rem;color:var(--blue);margin:.2rem 0 .5rem;}
|
||||
.welcome p{color:var(--muted);font-size:1rem;line-height:1.6;margin:0 auto 1.8rem;max-width:440px;}
|
||||
.wcards{display:flex;gap:1rem;flex-wrap:wrap;justify-content:center;}
|
||||
.wcard{flex:1;min-width:150px;max-width:180px;background:var(--card);border:1px solid var(--line);border-radius:14px;padding:1.2rem 1rem;cursor:pointer;transition:transform .12s,box-shadow .12s,border-color .12s;text-align:center;}
|
||||
.wcard:hover{transform:translateY(-3px);box-shadow:0 12px 28px rgba(20,30,60,.1);border-color:var(--brand);}
|
||||
.wcard .wi{width:46px;height:46px;border-radius:12px;display:grid;place-items:center;margin:0 auto .6rem;background:var(--blue-soft);color:var(--blue);}
|
||||
.wcard h3{margin:0 0 .2rem;font-size:.95rem;color:var(--blue);}
|
||||
.wcard p{margin:0;font-size:.78rem;color:var(--muted);line-height:1.4;}
|
||||
|
||||
.card{background:var(--card);border:1px solid var(--line);border-radius:16px;padding:2.2rem;box-shadow:0 6px 18px rgba(20,30,60,.05);text-align:center;max-width:520px;}
|
||||
.feat-icon{width:72px;height:72px;border-radius:20px;display:grid;place-items:center;margin:0 auto 1.2rem;}
|
||||
.feat-icon.yellow{background:#fff6d8;color:var(--brand-d);}
|
||||
.card h1{font-size:1.45rem;margin:0 0 .5rem;color:var(--blue);}
|
||||
.card p{color:var(--muted);font-size:.95rem;line-height:1.55;margin:0 auto 1.6rem;max-width:400px;}
|
||||
.pill-soon{display:inline-block;background:#fff6d8;color:var(--brand-d);font-size:.74rem;font-weight:700;padding:.25rem .7rem;border-radius:99px;letter-spacing:.03em;margin-bottom:1.2rem;}
|
||||
.btn{display:inline-flex;align-items:center;gap:.5rem;text-decoration:none;padding:.8rem 1.6rem;background:var(--brand);color:var(--ink);border:none;border-radius:11px;font-weight:700;font-size:.95rem;cursor:pointer;}
|
||||
.btn:hover{background:var(--brand-d);}
|
||||
.hint{margin-top:1.4rem;font-size:.8rem;color:var(--muted);}
|
||||
|
||||
/* conversation placeholder (selected chat, no backend yet) */
|
||||
.convo{flex-direction:column;display:flex;width:100%;height:100%;}
|
||||
.convo-head{display:flex;align-items:center;gap:.7rem;padding:.9rem 1.2rem;border-bottom:1px solid var(--line);background:var(--card);}
|
||||
.convo-back{border:none;background:var(--blue-soft);color:var(--blue);width:34px;height:34px;border-radius:9px;font-size:1.1rem;cursor:pointer;display:grid;place-items:center;flex:0 0 auto;}
|
||||
.convo-back:hover{background:#dbe6fb;}
|
||||
.convo-head .nm{font-weight:700;color:var(--ink);}
|
||||
.convo-head .st{font-size:.78rem;color:var(--muted);}
|
||||
.convo-body{flex:1;display:grid;place-items:center;text-align:center;color:var(--muted);padding:2rem;}
|
||||
.convo-body .big{font-size:2.4rem;margin-bottom:.4rem;}
|
||||
|
||||
/* ---- Login (shown on /home when logged out) ---- */
|
||||
.authwrap{flex:1 1 auto;display:none;align-items:center;justify-content:center;padding:1.5rem;min-height:0;}
|
||||
.authcard{background:var(--card);border:1px solid var(--line);border-radius:16px;padding:2rem;max-width:400px;width:100%;box-shadow:0 10px 30px rgba(20,30,60,.08);}
|
||||
.authcard h1{font-size:1.3rem;color:var(--blue);margin:0 0 .3rem;text-align:center;}
|
||||
.authcard .sub{color:var(--muted);font-size:.9rem;text-align:center;margin-bottom:1.2rem;}
|
||||
.authtabs{display:flex;gap:.5rem;margin-bottom:1.1rem;}
|
||||
.authtabs button{flex:1;background:#eef1f6;color:var(--muted);font-weight:600;border:none;border-radius:9px;padding:.5rem;cursor:pointer;font-size:.9rem;}
|
||||
.authtabs button.active{background:var(--blue);color:#fff;}
|
||||
.authcard .lbl{display:block;font-size:.74rem;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin:.7rem 0 .15rem;}
|
||||
.authcard input{width:100%;padding:.6rem .7rem;border-radius:10px;border:2px solid var(--line);background:#fbfcfe;color:var(--ink);font-size:.92rem;}
|
||||
.authcard input:focus{outline:none;border-color:var(--brand);}
|
||||
.authcard .gobtn{width:100%;margin-top:1rem;padding:.7rem;background:var(--brand);color:var(--ink);border:none;border-radius:10px;font-weight:700;cursor:pointer;font-size:.95rem;}
|
||||
.authcard .gobtn:hover{background:var(--brand-d);}
|
||||
.authcard .pwwrap{position:relative;} .authcard .pwwrap input{padding-right:2.6rem;}
|
||||
.authcard .eye{position:absolute;right:.35rem;top:50%;transform:translateY(-50%);background:none;border:none;padding:.3rem;width:auto;color:var(--muted);display:inline-flex;align-items:center;cursor:pointer;}
|
||||
.authcard .eye:hover{color:var(--blue);}
|
||||
.formerr{color:#b91c1c;font-weight:600;font-size:.88rem;margin:.7rem 0 0;min-height:1em;}
|
||||
.formerr.show{display:flex;align-items:center;gap:.5rem;background:#fee2e2;border:1px solid #fca5a5;border-radius:9px;padding:.6rem .75rem;animation:errShake .35s;}
|
||||
.formerr.show::before{content:"⚠";font-size:1rem;}
|
||||
@keyframes errShake{0%,100%{transform:translateX(0)}20%,60%{transform:translateX(-5px)}40%,80%{transform:translateX(5px)}}
|
||||
.hidden{display:none;}
|
||||
|
||||
/* ---- Loading / toast ---- */
|
||||
.loading{position:fixed;inset:0;display:grid;place-items:center;background:var(--bg);z-index:9000;color:var(--muted);font-size:.9rem;}
|
||||
.toast{position:fixed;left:50%;bottom:1.6rem;transform:translateX(-50%) translateY(1rem);background:var(--blue);color:#fff;padding:.7rem 1.2rem;border-radius:10px;font-size:.88rem;box-shadow:0 10px 28px rgba(0,0,0,.22);opacity:0;pointer-events:none;transition:opacity .2s,transform .2s;z-index:9500;}
|
||||
.toast.show{opacity:1;transform:translateX(-50%) translateY(0);}
|
||||
|
||||
@media (max-width:760px){
|
||||
.chatcol{width:260px;flex:0 0 260px;}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="loading" id="loading">Loading…</div>
|
||||
|
||||
<header>
|
||||
<div class="brandrow">
|
||||
<img src="/logo.png" alt="" style="height:46px;width:auto;max-width:190px;border-radius:8px;object-fit:contain;background:#fff;padding:5px 12px;image-rendering:-webkit-optimize-contrast" onerror="this.replaceWith(Object.assign(document.createElement('div'),{className:'logo',textContent:'B'}))">
|
||||
<div class="brand">BizGaze <span class="y">Connect</span></div>
|
||||
</div>
|
||||
<div id="hdrRight"></div>
|
||||
</header>
|
||||
|
||||
<div class="shell">
|
||||
<!-- ---------- Icon rail ---------- -->
|
||||
<nav class="rail" id="rail">
|
||||
<button class="railbtn active" data-tab="chat" data-tip="Chat" aria-label="Chat">
|
||||
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
||||
<span class="rdot" id="railUnread" style="display:none">0</span>
|
||||
</button>
|
||||
<button class="railbtn" data-tab="share" data-tip="Share Screen" aria-label="Share Screen">
|
||||
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
|
||||
<span class="livedot"></span>
|
||||
</button>
|
||||
<button class="railbtn" data-tab="connect" data-tip="Connect Screen" aria-label="Connect Screen">
|
||||
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/></svg>
|
||||
<span class="livedot"></span>
|
||||
</button>
|
||||
<button class="railbtn" data-tab="meeting" data-tip="Meeting" aria-label="Meeting">
|
||||
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg>
|
||||
</button>
|
||||
<div class="rail-spacer"></div>
|
||||
</nav>
|
||||
|
||||
<!-- ---------- Chat list (Chat tab only) ---------- -->
|
||||
<aside class="chatcol" id="chatcol">
|
||||
<div class="side-head">
|
||||
<div class="side-title">
|
||||
<h2>Chats</h2>
|
||||
<button class="newchat" id="newChat" title="New chat" aria-label="New chat">+</button>
|
||||
</div>
|
||||
<div class="search">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input id="chatSearch" placeholder="Search chats" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
<div class="chatlist" id="chatlist"></div>
|
||||
<div class="demo-note">💬 Chat is coming soon — showing sample conversations</div>
|
||||
</aside>
|
||||
|
||||
<!-- ---------- Main content ---------- -->
|
||||
<main class="content">
|
||||
<!-- Chat panel: welcome (no selection) OR conversation placeholder -->
|
||||
<div class="panel center active" data-panel="chat" id="chatPanel"></div>
|
||||
|
||||
<!-- Share -->
|
||||
<div class="panel" data-panel="share" id="sharePanel"></div>
|
||||
|
||||
<!-- Connect -->
|
||||
<div class="panel" data-panel="connect" id="connectPanel"></div>
|
||||
|
||||
<!-- Meeting -->
|
||||
<div class="panel center" data-panel="meeting">
|
||||
<div class="card">
|
||||
<div class="feat-icon yellow">
|
||||
<svg viewBox="0 0 24 24" width="34" height="34" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg>
|
||||
</div>
|
||||
<span class="pill-soon">COMING SOON</span>
|
||||
<h1>Meetings are on the way</h1>
|
||||
<p>Soon you'll be able to host multi-party video meetings with your BizGaze team and customers — right here, no install needed.</p>
|
||||
<button class="btn" id="notifyBtn">🔔 Notify me when it's ready</button>
|
||||
<div class="hint">In the meantime, use <b>Share Screen</b> or <b>Connect Screen</b> from the left.</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div class="authwrap" id="authwrap"></div>
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script>
|
||||
// ---------- Helpers ----------
|
||||
function pEsc(s){return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));}
|
||||
function initials(name){const p=String(name||'?').trim().split(/\s+/);return ((p[0]||'?')[0]+(p[1]?p[1][0]:'')).toUpperCase();}
|
||||
function firstName(name){return String(name||'').trim().split(/\s+/)[0]||'there';}
|
||||
const AV_COLORS=['#1F3B73','#2563eb','#0e7490','#7c3aed','#be185d','#b45309','#15803d','#9d174d'];
|
||||
function avColor(name){let h=0;for(const c of String(name))h=(h*31+c.charCodeAt(0))>>>0;return AV_COLORS[h%AV_COLORS.length];}
|
||||
|
||||
let toastTimer=null;
|
||||
function toast(msg){const t=document.getElementById('toast');t.textContent=msg;t.classList.add('show');clearTimeout(toastTimer);toastTimer=setTimeout(()=>t.classList.remove('show'),2600);}
|
||||
|
||||
// ---------- Profile dropdown (mirrors profileHTML()/wireProfile() from console.html) ----------
|
||||
function profileHTML(u){
|
||||
const display=u.name||u.email;
|
||||
return '<div class="profile"><button class="pbtn" id="pbtn">'
|
||||
+ '<span class="pav">'+pEsc(initials(display))+'</span>'
|
||||
+ pEsc(display)+' <span style="font-size:.65rem">▾</span></button>'
|
||||
+ '<div class="pmenu" id="pmenu">'
|
||||
+ '<div class="phead"><div class="n">'+pEsc(display)+'</div><div class="e">'+pEsc(u.email)+(u.role?' · '+pEsc(u.role):'')+'</div></div>'
|
||||
+ '<a href="/dashboard">Dashboard</a>'
|
||||
+ '<a class="danger" id="plogout">Logout</a>'
|
||||
+ '</div></div>';
|
||||
}
|
||||
function wireProfile(){
|
||||
const btn=document.getElementById('pbtn'),menu=document.getElementById('pmenu');
|
||||
if(!btn)return;
|
||||
btn.onclick=(e)=>{e.stopPropagation();menu.classList.toggle('open');};
|
||||
document.addEventListener('click',()=>menu.classList.remove('open'));
|
||||
const lo=document.getElementById('plogout');
|
||||
if(lo)lo.onclick=async()=>{try{await fetch('/api/logout',{method:'POST'});}catch(_){}location.href='/';};
|
||||
}
|
||||
|
||||
// ---------- Mock chat data (placeholder — no chat backend yet, see CLAUDE.md) ----------
|
||||
const CHATS=[
|
||||
{name:'Anwi Systems', msg:"Perfect, the screen share worked great. Thanks!", time:'9:42 AM', unread:0, online:true},
|
||||
{name:'Priya Sharma', msg:"Can you connect to my screen at 3pm?", time:'9:15 AM', unread:2, online:true},
|
||||
{name:'GAPL Group', msg:"You: I've shared the 6-digit code with you", time:'Yesterday', unread:0, online:false},
|
||||
{name:'Battery Doctors', msg:"The invoice module is throwing an error again", time:'Yesterday', unread:5, online:true},
|
||||
{name:'Ramesh Marketing', msg:"You: Let me know once you're at your desk", time:'Mon', unread:0, online:false},
|
||||
{name:'STC Support', msg:"Typing…", time:'Mon', unread:1, online:true},
|
||||
{name:'Samruddhi Traders',msg:"Thanks for the help earlier 👍", time:'Sun', unread:0, online:false},
|
||||
{name:'DMS 3.0 Team', msg:"You: Closing the ticket, all resolved", time:'Fri', unread:0, online:false},
|
||||
];
|
||||
let selectedChat=null; // index into CHATS, or null = welcome
|
||||
|
||||
const listEl=document.getElementById('chatlist');
|
||||
function chatRowHTML(c,i){
|
||||
const cls=['chat-row'];
|
||||
if(selectedChat===i)cls.push('active');
|
||||
if(c.unread>0)cls.push('unread');
|
||||
return '<div class="'+cls.join(' ')+'" data-i="'+i+'">'
|
||||
+ '<div class="avatar" style="background:'+avColor(c.name)+'">'+pEsc(initials(c.name))
|
||||
+ '<span class="dot'+(c.online?' on':'')+'"></span></div>'
|
||||
+ '<div class="chat-main">'
|
||||
+ '<div class="chat-top"><span class="chat-name">'+pEsc(c.name)+'</span><span class="chat-time">'+pEsc(c.time)+'</span></div>'
|
||||
+ '<div class="chat-bottom"><span class="chat-prev">'+pEsc(c.msg)+'</span>'
|
||||
+ (c.unread>0?'<span class="badge">'+c.unread+'</span>':'')+'</div>'
|
||||
+ '</div></div>';
|
||||
}
|
||||
function renderChats(filter){
|
||||
const q=(filter||'').trim().toLowerCase();
|
||||
const rows=CHATS.map((c,i)=>({c,i})).filter(({c})=>!q||c.name.toLowerCase().includes(q)||c.msg.toLowerCase().includes(q));
|
||||
listEl.innerHTML = rows.length
|
||||
? rows.map(({c,i})=>chatRowHTML(c,i)).join('')
|
||||
: '<div class="no-results">No chats match “'+pEsc(filter)+'”.</div>';
|
||||
listEl.querySelectorAll('.chat-row').forEach(row=>{
|
||||
row.onclick=()=>{
|
||||
selectedChat=+row.dataset.i;
|
||||
CHATS[selectedChat].unread=0;
|
||||
renderChats(document.getElementById('chatSearch').value);
|
||||
renderChatPanel();
|
||||
updateRailUnread();
|
||||
};
|
||||
});
|
||||
}
|
||||
function updateRailUnread(){
|
||||
const total=CHATS.reduce((a,c)=>a+(c.unread||0),0);
|
||||
const d=document.getElementById('railUnread');
|
||||
if(total>0){ d.textContent=total>99?'99+':total; d.style.display='grid'; }
|
||||
else d.style.display='none';
|
||||
}
|
||||
|
||||
// ---------- Chat main panel: welcome OR conversation placeholder ----------
|
||||
let ME={};
|
||||
function welcomeHTML(){
|
||||
return '<div class="welcome">'
|
||||
+ '<div class="wave">👋</div>'
|
||||
+ '<h1>Hi, '+pEsc(firstName(ME.name||ME.email))+'! Welcome to BizGaze Connect</h1>'
|
||||
+ '<p>Pick a conversation on the left to start chatting, or jump straight into a session from the sidebar.</p>'
|
||||
+ '<div class="wcards">'
|
||||
+ '<div class="wcard" data-go="share"><div class="wi"><svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg></div><h3>Share Screen</h3><p>Show your screen with a 6-digit code</p></div>'
|
||||
+ '<div class="wcard" data-go="connect"><div class="wi"><svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/></svg></div><h3>Connect Screen</h3><p>Enter a customer\'s code to help</p></div>'
|
||||
+ '<div class="wcard" data-go="meeting"><div class="wi"><svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg></div><h3>Meeting</h3><p>Multi-party video — coming soon</p></div>'
|
||||
+ '</div></div>';
|
||||
}
|
||||
function convoHTML(c){
|
||||
return '<div class="convo">'
|
||||
+ '<div class="convo-head">'
|
||||
+ '<button class="convo-back" id="convoBack" title="Back to home (Esc)" aria-label="Back to home">←</button>'
|
||||
+ '<div class="avatar" style="width:38px;height:38px;flex:0 0 38px;background:'+avColor(c.name)+'">'+pEsc(initials(c.name))+'<span class="dot'+(c.online?' on':'')+'"></span></div>'
|
||||
+ '<div><div class="nm">'+pEsc(c.name)+'</div><div class="st">'+(c.online?'Online':'Offline')+'</div></div>'
|
||||
+ '</div>'
|
||||
+ '<div class="convo-body"><div><div class="big">💬</div><div style="font-weight:600;color:var(--ink);margin-bottom:.3rem">Messaging is coming soon</div><div>Persistent 1:1 chat with '+pEsc(c.name)+' will live here.<br>For now, start a screen session from the left.</div></div></div>'
|
||||
+ '</div>';
|
||||
}
|
||||
function renderChatPanel(){
|
||||
const el=document.getElementById('chatPanel');
|
||||
if(selectedChat==null){ el.classList.add('center'); el.innerHTML=welcomeHTML(); wireWelcome(); }
|
||||
else { el.classList.remove('center'); el.innerHTML=convoHTML(CHATS[selectedChat]); const b=document.getElementById('convoBack'); if(b) b.onclick=showWelcome; }
|
||||
}
|
||||
function wireWelcome(){
|
||||
document.querySelectorAll('#chatPanel .wcard').forEach(card=>{
|
||||
card.onclick=()=>switchTab(card.dataset.go);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- Tabs (icon rail) ----------
|
||||
// Chat and Meeting are in-shell panels; Share and Connect load in the center panel via
|
||||
// a single, same-origin, lazily-loaded iframe (cheap isolation, no page navigation).
|
||||
const railBtns=document.querySelectorAll('.railbtn');
|
||||
const panels=document.querySelectorAll('.panel');
|
||||
const chatcol=document.getElementById('chatcol');
|
||||
let loaded={share:false,connect:false};
|
||||
function currentTab(){ const b=document.querySelector('.railbtn.active'); return b?b.dataset.tab:'chat'; }
|
||||
function switchTab(tab){
|
||||
railBtns.forEach(b=>b.classList.toggle('active',b.dataset.tab===tab));
|
||||
panels.forEach(p=>p.classList.toggle('active',p.dataset.panel===tab));
|
||||
chatcol.classList.toggle('hidden', tab!=='chat');
|
||||
// Lazy-load the embedded flows on first open; keep them mounted afterwards so a
|
||||
// live session survives tab switches.
|
||||
if(tab==='share' && !loaded.share){ document.getElementById('sharePanel').innerHTML='<iframe src="/share?embed=1" allow="camera; microphone; display-capture; clipboard-read; clipboard-write"></iframe>'; loaded.share=true; }
|
||||
if(tab==='connect' && !loaded.connect){ document.getElementById('connectPanel').innerHTML='<iframe src="/connect?embed=1" allow="camera; microphone; display-capture; clipboard-read; clipboard-write"></iframe>'; loaded.connect=true; }
|
||||
}
|
||||
function showWelcome(){ selectedChat=null; renderChats(document.getElementById('chatSearch').value); renderChatPanel(); updateRailUnread(); }
|
||||
railBtns.forEach(btn=>{ btn.onclick=()=>{
|
||||
const tab=btn.dataset.tab;
|
||||
// Re-clicking Chat (while already on it) returns to the welcome screen.
|
||||
if(tab==='chat' && currentTab()==='chat' && selectedChat!=null){ showWelcome(); }
|
||||
switchTab(tab);
|
||||
}; });
|
||||
// Esc clears the open conversation and brings back the welcome screen.
|
||||
document.addEventListener('keydown',(e)=>{ if(e.key==='Escape' && currentTab()==='chat' && selectedChat!=null){ showWelcome(); } });
|
||||
|
||||
// Embedded Share/Connect flows report session start/stop so the rail can show a "live"
|
||||
// dot — that's how you know a session is still running after switching to Chat.
|
||||
window.addEventListener('message',(e)=>{
|
||||
if(e.origin!==location.origin) return;
|
||||
const d=e.data;
|
||||
if(!d||d.type!=='bzc-session'||(d.flow!=='share'&&d.flow!=='connect')) return;
|
||||
const btn=document.querySelector('.railbtn[data-tab="'+d.flow+'"]');
|
||||
if(!btn) return;
|
||||
btn.classList.toggle('live', !!d.active);
|
||||
if(d.active && currentTab()!==d.flow){
|
||||
toast((d.flow==='share'?'Screen share':'Connection')+' is live — tap the highlighted icon to return');
|
||||
}
|
||||
});
|
||||
|
||||
// Sidebar + misc wiring
|
||||
document.getElementById('chatSearch').addEventListener('input',e=>renderChats(e.target.value));
|
||||
document.getElementById('newChat').onclick=()=>toast('New chat is coming soon');
|
||||
document.getElementById('notifyBtn').onclick=()=>toast("Thanks! We'll let you know when Meetings launches.");
|
||||
|
||||
// ---------- Login (shown here on /home when logged out) ----------
|
||||
const EYE_OFF='<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>';
|
||||
const EYE_ON='<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>';
|
||||
function pwField(id,ph){return '<div class="pwwrap"><input id="'+id+'" type="password" placeholder="'+ph+'"><button type="button" class="eye" data-for="'+id+'" aria-label="Show password"></button></div>';}
|
||||
function wireEyes(){document.querySelectorAll('.eye').forEach(b=>{if(b._w)return;b._w=1;b.innerHTML=EYE_OFF;b.onclick=()=>{const inp=document.getElementById(b.getAttribute('data-for'));if(!inp)return;const show=inp.type==='password';inp.type=show?'text':'password';b.innerHTML=show?EYE_ON:EYE_OFF;};});}
|
||||
function onEnter(ids,fn){ids.forEach(id=>{const el=document.getElementById(id);if(el)el.addEventListener('keydown',e=>{if(e.key==='Enter'){e.preventDefault();fn();}});});}
|
||||
function showErr(id,msg){const el=document.getElementById(id);el.textContent=msg;el.classList.add('show');}
|
||||
function clearErr(id){const el=document.getElementById(id);el.textContent='';el.classList.remove('show');}
|
||||
async function postJSON(path,body){const r=await fetch(path,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});const d=await r.json().catch(()=>({}));if(!r.ok)throw new Error(d.error||'request failed');return d;}
|
||||
async function renderLogin(){
|
||||
document.querySelector('.shell').style.display='none';
|
||||
const aw=document.getElementById('authwrap'); aw.style.display='flex';
|
||||
let regOpen=false; try{ regOpen=(await (await fetch('/api/setup-state')).json()).registrationOpen; }catch(_){}
|
||||
aw.innerHTML=`<div class="authcard">
|
||||
<h1>Welcome to BizGaze Connect</h1>
|
||||
<div class="sub">Sign in to access chats, screen share and connect.</div>
|
||||
${regOpen?`<div class="authtabs">
|
||||
<button id="tabLogin" class="active">Sign in</button>
|
||||
<button id="tabReg">Register team</button>
|
||||
</div>`:''}
|
||||
<div id="loginForm">
|
||||
<span class="lbl">Email</span><input id="li_email" type="email" placeholder="you@bizgaze.com">
|
||||
<span class="lbl">Password</span>${pwField('li_pw','password')}
|
||||
<label style="display:flex;align-items:center;gap:.5rem;margin:.7rem 0;color:var(--ink);font-size:.9rem;cursor:pointer"><input type="checkbox" id="li_remember" style="width:18px;height:18px;accent-color:var(--blue);margin:0"> Remember me on this device</label>
|
||||
<button class="gobtn" id="li_btn">Sign in</button>
|
||||
<p id="li_err" class="formerr"></p>
|
||||
</div>
|
||||
${regOpen?`<div id="regForm" class="hidden">
|
||||
<span class="lbl">Team name</span><input id="rg_team" placeholder="e.g. BizGaze Support">
|
||||
<span class="lbl">Email</span><input id="rg_email" type="email" placeholder="you@bizgaze.com">
|
||||
<span class="lbl">Password</span>${pwField('rg_pw','min 8 characters')}
|
||||
<button class="gobtn" id="rg_btn">Create team</button>
|
||||
<p id="rg_err" class="formerr"></p>
|
||||
</div>`:''}
|
||||
</div>`;
|
||||
document.getElementById('li_btn').onclick=doLogin;
|
||||
wireEyes();
|
||||
onEnter(['li_email','li_pw'],doLogin);
|
||||
if(regOpen){
|
||||
const lf=document.getElementById('loginForm'), rf=document.getElementById('regForm');
|
||||
const tl=document.getElementById('tabLogin'), tr=document.getElementById('tabReg');
|
||||
tl.onclick=()=>{lf.classList.remove('hidden');rf.classList.add('hidden');tl.classList.add('active');tr.classList.remove('active');};
|
||||
tr.onclick=()=>{rf.classList.remove('hidden');lf.classList.add('hidden');tr.classList.add('active');tl.classList.remove('active');};
|
||||
document.getElementById('rg_btn').onclick=doRegister;
|
||||
onEnter(['rg_team','rg_email','rg_pw'],doRegister);
|
||||
}
|
||||
}
|
||||
async function doLogin(){
|
||||
clearErr('li_err');
|
||||
try{
|
||||
await postJSON('/api/login',{email:document.getElementById('li_email').value,password:document.getElementById('li_pw').value,remember:document.getElementById('li_remember').checked});
|
||||
location.reload();
|
||||
}catch(e){ showErr('li_err', /invalid credentials/i.test(e.message)?'Incorrect email or password. Please try again.':e.message); }
|
||||
}
|
||||
async function doRegister(){
|
||||
clearErr('rg_err');
|
||||
try{
|
||||
await postJSON('/api/register',{email:document.getElementById('rg_email').value,password:document.getElementById('rg_pw').value,teamName:document.getElementById('rg_team').value});
|
||||
await postJSON('/api/login',{email:document.getElementById('rg_email').value,password:document.getElementById('rg_pw').value});
|
||||
location.reload();
|
||||
}catch(e){ showErr('rg_err', e.message); }
|
||||
}
|
||||
|
||||
// ---------- Boot: show the app if signed in, otherwise the login ----------
|
||||
(async function(){
|
||||
let me=null;
|
||||
try{ const r=await fetch('/api/me'); if(r.ok) me=await r.json(); }catch(_){}
|
||||
if(!me){ await renderLogin(); document.getElementById('loading').style.display='none'; return; }
|
||||
ME=me;
|
||||
document.getElementById('hdrRight').innerHTML=profileHTML(me);
|
||||
wireProfile();
|
||||
renderChats('');
|
||||
renderChatPanel();
|
||||
updateRailUnread();
|
||||
document.getElementById('loading').style.display='none';
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+27
-19
@@ -17,14 +17,19 @@
|
||||
.inner{max-width:780px;width:100%;text-align:center;}
|
||||
h1{color:var(--blue);font-size:1.8rem;margin:0 0 .4rem;}
|
||||
.sub{color:var(--muted);margin-bottom:2.2rem;}
|
||||
.ssobtn{display:inline-flex;align-items:center;justify-content:center;gap:.6rem;background:var(--blue);color:#fff;text-decoration:none;font-weight:700;font-size:1.02rem;padding:.95rem 2rem;border-radius:12px;box-shadow:0 10px 26px rgba(31,59,115,.28);transition:transform .12s,box-shadow .12s,background .12s;}
|
||||
.ssobtn:hover{transform:translateY(-2px);box-shadow:0 16px 34px rgba(31,59,115,.34);background:var(--blue-d);}
|
||||
.ssobtn .bmark{width:26px;height:26px;border-radius:7px;background:var(--brand);color:var(--blue);display:grid;place-items:center;font-weight:800;font-size:.9rem;}
|
||||
.divider{display:flex;align-items:center;gap:1rem;color:var(--muted);font-size:.85rem;max-width:360px;margin:1.8rem auto;}
|
||||
.divider::before,.divider::after{content:"";flex:1;height:1px;background:var(--line);}
|
||||
.choices{display:flex;gap:1.4rem;flex-wrap:wrap;justify-content:center;}
|
||||
.choice{flex:1;min-width:260px;max-width:340px;background:#fff;border:1px solid var(--line);border-radius:18px;padding:2.2rem 1.6rem;text-decoration:none;color:var(--ink);box-shadow:0 10px 30px rgba(20,30,60,.06);transition:transform .12s,box-shadow .12s;}
|
||||
.choice{flex:1;min-width:260px;max-width:360px;background:#fff;border:1px solid var(--line);border-radius:18px;padding:1.8rem 1.6rem;text-decoration:none;color:var(--ink);box-shadow:0 10px 30px rgba(20,30,60,.06);transition:transform .12s,box-shadow .12s,border-color .12s;display:flex;align-items:center;gap:1.1rem;text-align:left;}
|
||||
.choice:hover{transform:translateY(-3px);box-shadow:0 16px 38px rgba(20,30,60,.12);border-color:var(--brand);}
|
||||
.icon{width:66px;height:66px;border-radius:18px;display:grid;place-items:center;margin:0 auto 1.1rem;}
|
||||
.icon{width:56px;height:56px;flex:0 0 56px;border-radius:16px;display:grid;place-items:center;}
|
||||
.icon.share{background:#FFF7DA;} .icon.connect{background:var(--blue-soft);}
|
||||
.icon svg{width:34px;height:34px;}
|
||||
.choice h3{margin:.2rem 0 .4rem;color:var(--blue);font-size:1.15rem;}
|
||||
.choice p{margin:0;color:var(--muted);font-size:.9rem;line-height:1.5;}
|
||||
.icon svg{width:30px;height:30px;}
|
||||
.choice h3{margin:0 0 .25rem;color:var(--blue);font-size:1.1rem;}
|
||||
.choice p{margin:0;color:var(--muted);font-size:.88rem;line-height:1.45;}
|
||||
.foot{color:var(--muted);font-size:.82rem;margin-top:2.4rem;}
|
||||
footer{text-align:center;color:#9aa3b2;font-size:.8rem;padding:1rem;}
|
||||
.profile{position:relative}
|
||||
@@ -43,35 +48,38 @@
|
||||
<img src="/logo.png" alt="" style="height:40px;width:auto;max-width:170px;border-radius:8px;object-fit:contain;background:#fff;padding:4px 10px" onerror="this.style.display='none'">
|
||||
<div class="brand">BizGaze <span>Support</span></div>
|
||||
</div>
|
||||
<div id="authArea"><a class="signin" href="/console">Staff sign in</a></div>
|
||||
<div id="authArea"></div>
|
||||
</header>
|
||||
<div class="wrap">
|
||||
<div class="inner">
|
||||
<h1>How can we help you today?</h1>
|
||||
<div class="sub">Secure remote support — no downloads, you stay in control.</div>
|
||||
<div class="choices">
|
||||
<h1>Welcome to BizGaze Connect</h1>
|
||||
<div class="sub">Chat, meetings and secure remote support — for the BizGaze ecosystem.</div>
|
||||
<!-- Stub SSO: routes to staff login for now; swap href to /sso once BizGaze SSO is wired. -->
|
||||
<a class="ssobtn" id="ssoBtn" href="/home"><span class="bmark">B</span> Log in with BizGaze</a>
|
||||
<div class="divider">need support? no account required</div>
|
||||
<div class="choices" style="max-width:400px;margin:0 auto">
|
||||
<a class="choice" href="/share">
|
||||
<div class="icon share"><svg viewBox="0 0 24 24" fill="none" stroke="#BA7515" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg></div>
|
||||
<h3>Share my screen</h3>
|
||||
<p>You need help. Get a one-time code and show your screen to a BizGaze support agent.</p>
|
||||
</a>
|
||||
<a class="choice" href="/connect">
|
||||
<div class="icon connect"><svg viewBox="0 0 24 24" fill="none" stroke="#1F3B73" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 17V7a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v10"/><path d="M2 21h20"/><path d="m9 9 3 3-3 3"/></svg></div>
|
||||
<h3>Connect to a screen</h3>
|
||||
<p>You're a support agent. Sign in, then enter the customer's code to view their screen.</p>
|
||||
<div><h3>Share my screen</h3><p>Get a one-time code and show your screen to a BizGaze support agent — no login, no download.</p></div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="foot">🔒 Screen sharing only starts after the customer approves it, and can be stopped anytime.</div>
|
||||
<div class="foot">🔒 Screen sharing only starts after you approve it, and can be stopped anytime.</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer>© BizGaze · Remote Support</footer>
|
||||
<script>
|
||||
function pEsc(s){return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));}
|
||||
function profileHTML(name){return '<div class="profile"><button class="pbtn" id="pbtn">'+pEsc(name)+' <span style="font-size:.65rem">▾</span></button><div class="pmenu" id="pmenu"><a href="/console">Console / Dashboard</a><a id="plogout" class="danger">Logout</a></div></div>';}
|
||||
function profileHTML(name){return '<div class="profile"><button class="pbtn" id="pbtn">'+pEsc(name)+' <span style="font-size:.65rem">▾</span></button><div class="pmenu" id="pmenu"><a href="/home">Home</a><a href="/dashboard">Dashboard</a><a id="plogout" class="danger">Logout</a></div></div>';}
|
||||
function wireProfile(){const btn=document.getElementById('pbtn'),menu=document.getElementById('pmenu');if(!btn)return;btn.onclick=(e)=>{e.stopPropagation();menu.classList.toggle('open');};document.addEventListener('click',()=>menu.classList.remove('open'));const lo=document.getElementById('plogout');if(lo)lo.onclick=async()=>{try{await fetch('/api/logout',{method:'POST'});}catch(_){}location.href='/';};}
|
||||
function makeBrandClickable(){document.querySelectorAll('.brandrow,.wordmark').forEach(el=>{el.style.cursor='pointer';el.addEventListener('click',()=>{location.href='/';});});}
|
||||
makeBrandClickable();
|
||||
(async function(){try{const r=await fetch('/api/me');if(r.ok){const me=await r.json();document.getElementById('authArea').innerHTML=profileHTML(me.name||me.email);wireProfile();}}catch(_){}})();
|
||||
(async function(){try{const r=await fetch('/api/me');if(r.ok){const me=await r.json();
|
||||
document.getElementById('authArea').innerHTML=profileHTML(me.name||me.email);wireProfile();
|
||||
// Already signed in: swap the login CTA for an "enter app" CTA.
|
||||
const b=document.getElementById('ssoBtn'); if(b){ b.innerHTML='Open BizGaze Connect →'; b.href='/home'; }
|
||||
const h=document.querySelector('.inner h1'); if(h){ const fn=String(me.name||'').trim().split(/\s+/)[0]; h.textContent='Welcome back'+(fn?', '+fn:'')+'!'; }
|
||||
const dv=document.querySelector('.divider'); if(dv) dv.textContent='need to help someone? share your screen';
|
||||
}}catch(_){}})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -32,6 +32,10 @@
|
||||
.foot{color:var(--muted);font-size:.8rem;margin-top:1.4rem;}
|
||||
.indicator{position:fixed;top:0;left:0;right:0;background:#dc2626;color:#fff;text-align:center;padding:.5rem;font-size:.9rem;display:none;font-weight:600;z-index:9;}
|
||||
.indicator.show{display:block;}
|
||||
/* Embedded inside the home shell: hide own chrome (the shell provides it). */
|
||||
html.embed .brandpanel{display:none!important;}
|
||||
html.embed #homeLink{display:none!important;}
|
||||
html.embed .panelside{flex:1;}
|
||||
@media(max-width:860px){ .stage{flex-direction:column;} .brandpanel{padding:2rem;min-height:auto;} .mark{width:60px;height:60px;border-radius:16px;font-size:1.8rem;margin-bottom:.7rem;} .wordmark{font-size:1.5rem;} .tagline{display:none;} }
|
||||
.profile{position:relative}
|
||||
.profile .pbtn{display:flex;align-items:center;gap:.4rem;background:rgba(255,255,255,.14);color:#fff;border:1px solid #46598c;border-radius:10px;padding:.45rem .85rem;font-weight:600;font-size:.88rem;cursor:pointer}
|
||||
@@ -44,6 +48,7 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script>if(new URLSearchParams(location.search).get('embed')==='1')document.documentElement.classList.add('embed');</script>
|
||||
<div class="indicator" id="indicator">● Your screen is being shared — close this tab anytime to stop</div>
|
||||
<a href="/" id="homeLink" style="position:fixed;top:14px;left:16px;z-index:50;display:inline-flex;align-items:center;gap:6px;background:rgba(255,255,255,.92);color:#1F3B73;text-decoration:none;font-weight:600;font-size:.86rem;padding:.45rem .8rem;border-radius:10px;box-shadow:0 4px 12px rgba(0,0,0,.15)">← Home</a>
|
||||
<div class="stage">
|
||||
@@ -77,7 +82,10 @@ const IS_MOBILE=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|M
|
||||
let __icePromise=Promise.resolve();try{__icePromise=fetch('/api/ice').then(r=>r.ok?r.json():null).then(c=>{if(c&&c.iceServers&&IS_MOBILE)ICE=c;}).catch(()=>{});}catch(_){}
|
||||
async function ensureIce(){try{await __icePromise;}catch(_){}return ICE;}
|
||||
function pEsc(s){return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));}
|
||||
function profileHTML(name){return '<div class="profile"><button class="pbtn" id="pbtn">'+pEsc(name)+' <span style="font-size:.65rem">▾</span></button><div class="pmenu" id="pmenu"><a href="/console">Console / Dashboard</a><a id="plogout" class="danger">Logout</a></div></div>';}
|
||||
// When embedded in the home shell, tell the parent when a session is live so the
|
||||
// rail can show a "return here" indicator.
|
||||
function bzcSession(active){try{if(window.parent&&window.parent!==window)window.parent.postMessage({type:'bzc-session',flow:'share',active:!!active},location.origin);}catch(_){}}
|
||||
function profileHTML(name){return '<div class="profile"><button class="pbtn" id="pbtn">'+pEsc(name)+' <span style="font-size:.65rem">▾</span></button><div class="pmenu" id="pmenu"><a href="/home">Home</a><a href="/dashboard">Dashboard</a><a id="plogout" class="danger">Logout</a></div></div>';}
|
||||
function wireProfile(){const btn=document.getElementById('pbtn'),menu=document.getElementById('pmenu');if(!btn)return;btn.onclick=(e)=>{e.stopPropagation();menu.classList.toggle('open');};document.addEventListener('click',()=>menu.classList.remove('open'));const lo=document.getElementById('plogout');if(lo)lo.onclick=async()=>{try{await fetch('/api/logout',{method:'POST'});}catch(_){}location.href='/';};}
|
||||
function makeBrandClickable(){document.querySelectorAll('.brandrow,.wordmark').forEach(el=>{el.style.cursor='pointer';el.addEventListener('click',()=>{location.href='/';});});}
|
||||
makeBrandClickable();
|
||||
@@ -151,7 +159,7 @@ async function startStreaming(){
|
||||
if(mic){ window.__mic=mic; try{mic.getAudioTracks().forEach(t=>localStream.addTrack(t));}catch(_){} }
|
||||
}
|
||||
await ensureIce();
|
||||
indicator.classList.add('show'); setStatus('You are now sharing your screen with your agent.','on');
|
||||
indicator.classList.add('show'); setStatus('You are now sharing your screen with your agent.','on'); bzcSession(true);
|
||||
{ const hl=document.getElementById('homeLink'); if(hl) hl.style.display='none'; }
|
||||
window.onbeforeunload=function(){ if((localStream||document.getElementById('sessionBar'))&&!sessionOver){ return 'Leaving or refreshing this page will end your screen sharing session.'; } };
|
||||
pc=new RTCPeerConnection(ICE);
|
||||
@@ -198,7 +206,7 @@ function recNotice(on){
|
||||
} else { clearInterval(recTimerInt); recTimerInt=null; if(n) n.remove(); }
|
||||
}
|
||||
function endShareSession(msgText){
|
||||
sessionOver=true; window.onbeforeunload=null; { const hl=document.getElementById('homeLink'); if(hl) hl.style.display=''; } try{recNotice(false);stopCustTranscription();}catch(_){}
|
||||
sessionOver=true; window.onbeforeunload=null; bzcSession(false); { const hl=document.getElementById('homeLink'); if(hl) hl.style.display=''; } try{recNotice(false);stopCustTranscription();}catch(_){}
|
||||
removeSessionUI();
|
||||
indicator.classList.remove('show');
|
||||
if(window.__mic){try{window.__mic.getTracks().forEach(t=>t.stop());}catch(_){}window.__mic=null;}
|
||||
@@ -207,7 +215,7 @@ function endShareSession(msgText){
|
||||
var card=document.querySelector('.panelside .card');
|
||||
if(card){ card.innerHTML='<h1 style="color:var(--blue)">Session ended</h1><div class="sub">'+esc(msgText||'The session has ended.')+'</div><button onclick="location.reload()" style="width:100%;margin-top:.4rem">Get a new code</button>'; }
|
||||
}
|
||||
function teardown(){sessionOver=true;window.onbeforeunload=null;{const hl=document.getElementById('homeLink');if(hl)hl.style.display='';}try{recNotice(false);stopCustTranscription();}catch(_){}indicator.classList.remove('show');removeSessionUI();if(window.__mic){window.__mic.getTracks().forEach(t=>t.stop());window.__mic=null;}if(localStream){localStream.getTracks().forEach(t=>t.stop());localStream=null;}if(pc){pc.close();pc=null;}consentBox.innerHTML='';setStatus('Session ended. Refresh this page to get a new code.');}
|
||||
function teardown(){sessionOver=true;window.onbeforeunload=null;bzcSession(false);{const hl=document.getElementById('homeLink');if(hl)hl.style.display='';}try{recNotice(false);stopCustTranscription();}catch(_){}indicator.classList.remove('show');removeSessionUI();if(window.__mic){window.__mic.getTracks().forEach(t=>t.stop());window.__mic=null;}if(localStream){localStream.getTracks().forEach(t=>t.stop());localStream=null;}if(pc){pc.close();pc=null;}consentBox.innerHTML='';setStatus('Session ended. Refresh this page to get a new code.');}
|
||||
|
||||
let chatOpen=false;
|
||||
const SVG_MIC='<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="2" width="6" height="11" rx="3"/><path d="M5 10a7 7 0 0 0 14 0"/><line x1="12" y1="19" x2="12" y2="22"/></svg>';
|
||||
|
||||
Reference in New Issue
Block a user