Compact live visitor panel
This commit is contained in:
+27
-11
@@ -381,16 +381,18 @@ label { display: grid; gap: 6px; color: var(--muted); font-size: 13px; }
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
.visitor-card {
|
.visitor-card {
|
||||||
padding: 12px 14px;
|
padding: 10px 12px;
|
||||||
border-bottom: 1px solid var(--line);
|
border-bottom: 1px solid var(--line);
|
||||||
background: white;
|
background: white;
|
||||||
}
|
}
|
||||||
|
.visitor-card.clickable { cursor: pointer; }
|
||||||
|
.visitor-card.clickable:hover { background: #f8fbf9; }
|
||||||
.visitor-card-head {
|
.visitor-card-head {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 34px minmax(0, 1fr) 10px;
|
grid-template-columns: 34px minmax(0, 1fr) 10px;
|
||||||
gap: 9px;
|
gap: 9px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 7px;
|
||||||
}
|
}
|
||||||
.visitor-card-head strong,
|
.visitor-card-head strong,
|
||||||
.visitor-card-head small {
|
.visitor-card-head small {
|
||||||
@@ -418,21 +420,35 @@ label { display: grid; gap: 6px; color: var(--muted); font-size: 13px; }
|
|||||||
background: #2f9e44;
|
background: #2f9e44;
|
||||||
box-shadow: 0 0 0 4px rgb(47 158 68 / 14%);
|
box-shadow: 0 0 0 4px rgb(47 158 68 / 14%);
|
||||||
}
|
}
|
||||||
.visitor-card dl {
|
.visitor-compact-meta {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: 68px minmax(0, 1fr);
|
align-items: center;
|
||||||
gap: 5px 8px;
|
gap: 7px;
|
||||||
margin: 0;
|
min-width: 0;
|
||||||
|
color: var(--muted);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
.visitor-card dt { color: var(--muted); }
|
.visitor-compact-meta span {
|
||||||
.visitor-card dd {
|
flex: 0 0 auto;
|
||||||
min-width: 0;
|
max-width: 72px;
|
||||||
margin: 0;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
.visitor-page,
|
||||||
|
.visitor-referrer {
|
||||||
|
display: block;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.visitor-page { margin-top: 6px; }
|
||||||
|
.visitor-referrer {
|
||||||
|
margin-top: 3px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
.visitor-card a { color: var(--ink); text-decoration: none; }
|
.visitor-card a { color: var(--ink); text-decoration: none; }
|
||||||
.visitor-card a:hover { color: var(--brand); text-decoration: underline; }
|
.visitor-card a:hover { color: var(--brand); text-decoration: underline; }
|
||||||
.message {
|
.message {
|
||||||
|
|||||||
+17
-15
@@ -199,6 +199,14 @@ $("#messages").addEventListener("click", (event) => {
|
|||||||
openPhotoPreview(preview.dataset.photoUrl, preview.dataset.photoName);
|
openPhotoPreview(preview.dataset.photoUrl, preview.dataset.photoName);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#visitorList").addEventListener("click", async (event) => {
|
||||||
|
if (event.target.closest("a")) return;
|
||||||
|
const card = event.target.closest("[data-visitor-conversation]");
|
||||||
|
const conversationId = card?.dataset.visitorConversation;
|
||||||
|
if (!conversationId) return;
|
||||||
|
await openConversation(conversationId);
|
||||||
|
});
|
||||||
|
|
||||||
async function showApp() {
|
async function showApp() {
|
||||||
$("#loginView").classList.add("hidden");
|
$("#loginView").classList.add("hidden");
|
||||||
$("#appView").classList.remove("hidden");
|
$("#appView").classList.remove("hidden");
|
||||||
@@ -320,7 +328,7 @@ function renderConversations() {
|
|||||||
function renderVisitors() {
|
function renderVisitors() {
|
||||||
$("#visitorCount").textContent = `${state.visitors.length} online`;
|
$("#visitorCount").textContent = `${state.visitors.length} online`;
|
||||||
$("#visitorList").innerHTML = state.visitors.length ? state.visitors.map((visitor) => `
|
$("#visitorList").innerHTML = state.visitors.length ? state.visitors.map((visitor) => `
|
||||||
<article class="visitor-card">
|
<article class="visitor-card ${visitor.lastConversationId ? "clickable" : ""}" data-visitor-conversation="${escapeHtml(visitor.lastConversationId || "")}">
|
||||||
<div class="visitor-card-head">
|
<div class="visitor-card-head">
|
||||||
<span class="visitor-avatar">${escapeHtml(visitor.initials || "??")}</span>
|
<span class="visitor-avatar">${escapeHtml(visitor.initials || "??")}</span>
|
||||||
<div>
|
<div>
|
||||||
@@ -329,20 +337,14 @@ function renderVisitors() {
|
|||||||
</div>
|
</div>
|
||||||
<span class="online-dot" title="Online"></span>
|
<span class="online-dot" title="Online"></span>
|
||||||
</div>
|
</div>
|
||||||
<dl>
|
<div class="visitor-compact-meta">
|
||||||
<dt>Zeme</dt>
|
<span>${visitor.countryFlag || escapeHtml(visitor.countryCode || "?")}</span>
|
||||||
<dd>${visitor.countryFlag || ""} ${escapeHtml(visitor.countryCode || "?")}</dd>
|
<span>${formatDuration(visitor.sessionSeconds)}</span>
|
||||||
<dt>Stranka</dt>
|
<span>${Number(visitor.visitCount || visitor.pageCount || 1)} navst.</span>
|
||||||
<dd><a href="${escapeHtml(visitor.currentUrl || "#")}" target="_blank" rel="noreferrer">${escapeHtml(pageTitle(visitor))}</a></dd>
|
<span>${Number(visitor.conversationCount || 0)} chaty</span>
|
||||||
<dt>Prisiel z</dt>
|
</div>
|
||||||
<dd>${visitor.referrer ? `<a href="${escapeHtml(visitor.referrer)}" target="_blank" rel="noreferrer">${escapeHtml(referrerLabel(visitor.referrer))}</a>` : "primy vstup"}</dd>
|
<a class="visitor-page" href="${escapeHtml(visitor.currentUrl || "#")}" target="_blank" rel="noreferrer">${escapeHtml(pageTitle(visitor))}</a>
|
||||||
<dt>Cas</dt>
|
<div class="visitor-referrer">${visitor.referrer ? escapeHtml(referrerLabel(visitor.referrer)) : "primy vstup"}${visitor.lastConversationId ? "" : " · bez chatu"}</div>
|
||||||
<dd>${formatDuration(visitor.sessionSeconds)}</dd>
|
|
||||||
<dt>Navstevy</dt>
|
|
||||||
<dd>${Number(visitor.visitCount || visitor.pageCount || 1)}</dd>
|
|
||||||
<dt>Chaty</dt>
|
|
||||||
<dd>${Number(visitor.conversationCount || 0)}</dd>
|
|
||||||
</dl>
|
|
||||||
</article>
|
</article>
|
||||||
`).join("") : `<p class="empty-list">Zatim nikdo online.</p>`;
|
`).join("") : `<p class="empty-list">Zatim nikdo online.</p>`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -625,6 +625,7 @@ function updateVisitorContext(conversationId, body) {
|
|||||||
function currentVisitors() {
|
function currentVisitors() {
|
||||||
const rows = db.prepare(`
|
const rows = db.prepare(`
|
||||||
SELECT vs.*, s.site_key,
|
SELECT vs.*, s.site_key,
|
||||||
|
(SELECT c.public_id FROM conversations c WHERE c.site_id = vs.site_id AND c.visitor_token = vs.visitor_token ORDER BY datetime(c.last_message_at) DESC, c.id DESC LIMIT 1) AS last_conversation_id,
|
||||||
(SELECT COUNT(*) FROM conversations c WHERE c.site_id = vs.site_id AND c.visitor_token = vs.visitor_token) AS visitor_conversation_count
|
(SELECT COUNT(*) FROM conversations c WHERE c.site_id = vs.site_id AND c.visitor_token = vs.visitor_token) AS visitor_conversation_count
|
||||||
FROM visitor_sessions vs
|
FROM visitor_sessions vs
|
||||||
JOIN sites s ON s.id = vs.site_id
|
JOIN sites s ON s.id = vs.site_id
|
||||||
@@ -650,6 +651,7 @@ function formatVisitorSession(row) {
|
|||||||
pageCount: new Set(history.map((item) => item.url).filter(Boolean)).size,
|
pageCount: new Set(history.map((item) => item.url).filter(Boolean)).size,
|
||||||
visitCount: Math.max(1, history.length),
|
visitCount: Math.max(1, history.length),
|
||||||
conversationCount: row.visitor_conversation_count || 0,
|
conversationCount: row.visitor_conversation_count || 0,
|
||||||
|
lastConversationId: row.last_conversation_id,
|
||||||
device,
|
device,
|
||||||
language: row.language,
|
language: row.language,
|
||||||
timezone: row.timezone,
|
timezone: row.timezone,
|
||||||
|
|||||||
Reference in New Issue
Block a user