From 571770b1c3f82d6fe9bee752ac4da0d25c59e005 Mon Sep 17 00:00:00 2001 From: rajchales-monito Date: Thu, 23 Jul 2026 23:15:52 +0200 Subject: [PATCH] Improve admin visitor context layout --- public/admin.css | 110 ++++++++++++++++++++++++++++++++++++++++++---- public/admin.html | 1 - public/admin.js | 82 +++++++++++++++++++++++++++++++--- server.js | 19 +++++++- 4 files changed, 197 insertions(+), 15 deletions(-) diff --git a/public/admin.css b/public/admin.css index 5e498cd..4a0146a 100644 --- a/public/admin.css +++ b/public/admin.css @@ -10,7 +10,7 @@ } * { box-sizing: border-box; } -body { margin: 0; background: var(--bg); color: var(--ink); } +body { margin: 0; background: var(--bg); color: var(--ink); overflow: hidden; } button, input, textarea, select { font: inherit; } button { border: 0; @@ -47,15 +47,19 @@ label { display: grid; gap: 6px; color: var(--muted); font-size: 13px; } .error { min-height: 20px; color: #b42318; margin: 0; } .app { - min-height: 100vh; + height: 100vh; + min-height: 0; display: grid; grid-template-columns: 330px minmax(0, 1fr); + overflow: hidden; } .sidebar { border-right: 1px solid var(--line); background: var(--panel); display: grid; grid-template-rows: auto auto auto minmax(0, 1fr); + min-height: 0; + overflow: hidden; } .brand, .sitebar { display: flex; @@ -76,7 +80,7 @@ label { display: grid; gap: 6px; color: var(--muted); font-size: 13px; } } .list-tools label { color: var(--muted); } -.conversation-list { overflow: auto; } +.conversation-list { overflow: auto; min-height: 0; } .conversation-item { border: 0; border-bottom: 1px solid var(--line); @@ -173,10 +177,12 @@ label { display: grid; gap: 6px; color: var(--muted); font-size: 13px; } font-size: 13px; } -.conversation { display: grid; grid-template-rows: auto minmax(0, 1fr) auto auto; min-width: 0; } +.conversation { display: grid; grid-template-rows: auto minmax(0, 1fr) auto auto; min-width: 0; min-height: 0; overflow: hidden; } .conversation-header { min-height: 74px; - padding: 16px 20px; + max-height: 42vh; + overflow: auto; + padding: 14px 20px; background: white; border-bottom: 1px solid var(--line); display: flex; @@ -186,8 +192,95 @@ label { display: grid; gap: 6px; color: var(--muted); font-size: 13px; } .conversation-header h2 { margin: 0 0 5px; font-size: 20px; } .conversation-header p { margin: 0; color: var(--muted); font-size: 13px; } .conversation-header select { width: 150px; align-self: start; } +.visitor-head { width: 100%; min-width: 0; display: grid; gap: 7px; } +.visitor-title-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } +.visitor-title-row h2 { margin: 0; } +.country-pill { + border: 1px solid #e4ebe6; + border-radius: 999px; + padding: 3px 8px; + background: #f8fbf9; + color: var(--muted); + font-size: 12px; +} +.visitor-quick { + display: flex; + flex-wrap: wrap; + gap: 6px; + color: var(--muted); + font-size: 12px; +} +.visitor-quick span { + border: 1px solid #e4ebe6; + border-radius: 999px; + padding: 2px 7px; + background: #f8fbf9; +} +.visitor-details { + max-width: 980px; + color: var(--muted); + font-size: 12px; +} +.visitor-details summary { + width: fit-content; + display: inline-flex; + align-items: center; + gap: 6px; + cursor: pointer; + user-select: none; + color: var(--ink); +} +.visitor-details summary::-webkit-details-marker { display: none; } +.visitor-details summary span { + width: 20px; + height: 20px; + display: grid; + place-items: center; + border-radius: 50%; + background: var(--brand); + color: white; + font-size: 12px; + font-weight: 700; +} +.visitor-detail-grid { + display: grid; + grid-template-columns: 76px minmax(0, 1fr); + gap: 5px 10px; + margin-top: 8px; +} +.visitor-detail-grid span { color: var(--muted); } +.visitor-detail-grid a, +.history-list a { + min-width: 0; + color: var(--brand); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.visitor-detail-grid em { + min-width: 0; + color: var(--muted); + font-style: normal; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.history-list { + margin: 8px 0 0; + padding-left: 20px; + display: grid; + gap: 4px; +} +.history-list li { + min-width: 0; + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 10px; +} +.history-list small { color: var(--muted); } .messages { overflow: auto; + min-height: 0; padding: 20px; display: flex; flex-direction: column; @@ -400,7 +493,8 @@ pre { } @media (max-width: 820px) { - .app { grid-template-columns: 1fr; } - .sidebar { min-height: 42vh; border-right: 0; border-bottom: 1px solid var(--line); } - .conversation { min-height: 58vh; } + body { overflow: auto; } + .app { height: auto; min-height: 100vh; grid-template-columns: 1fr; overflow: visible; } + .sidebar { height: 42vh; border-right: 0; border-bottom: 1px solid var(--line); } + .conversation { height: 58vh; } } diff --git a/public/admin.html b/public/admin.html index 6b7d2e5..b853151 100644 --- a/public/admin.html +++ b/public/admin.html @@ -40,7 +40,6 @@ - diff --git a/public/admin.js b/public/admin.js index 9de836b..438ee88 100644 --- a/public/admin.js +++ b/public/admin.js @@ -270,10 +270,17 @@ function renderConversations() { function renderActive() { const { conversation, messages } = state.active; $("#conversationHeader").innerHTML = ` -
-

${escapeHtml(conversation.visitor.name || conversation.visitor.email || "Navstevnik")}

-

${visitorCountry(conversation)} ${escapeHtml(conversation.currentUrl || "")}

-

${escapeHtml(conversation.device.userAgent || "")}

+
+
+

${escapeHtml(conversation.visitor.name || conversation.visitor.email || "Navstevnik")}

+ ${visitorCountry(conversation)} +
+
+ ${formatDuration(conversation.sessionSeconds)} + ${Number(conversation.pageCount || 0)} str. + ${Number(conversation.visitorConversationCount || 1)}x chat +
+ ${visitorDetails(conversation)}
`; $("#replyForm").classList.remove("hidden"); @@ -306,10 +313,37 @@ function statusButtons(item) { } function visitorCountry(item) { - const countryCode = item.countryCode || countryFromLanguage(item.language); + const countryCode = item.countryCode || countryFromLanguage(item.language) || countryFromTimezone(item.timezone); return countryCode ? `${flag(countryCode)} ${escapeHtml(countryCode)}` : "zeme ?"; } +function visitorDetails(conversation) { + const history = Array.isArray(conversation.browsingHistory) ? conversation.browsingHistory : []; + const historyItems = history.length ? history.slice().reverse().map((item) => ` +
  • + ${escapeHtml(pageLabel(item))} + ${escapeHtml(formatVisitedAt(item.at))} +
  • + `).join("") : `
  • Zatim bez historie.
  • `; + return ` +
    + + + Detail navstevnika + +
    + Aktualne + ${escapeHtml(shortUrl(conversation.currentUrl) || "-")} + Prisiel z + ${conversation.referrer ? `${escapeHtml(shortUrl(conversation.referrer))}` : `primy vstup / nezname`} + Zarizeni + ${escapeHtml(deviceLabel(conversation.device, conversation.language, conversation.timezone))} +
    +
      ${historyItems}
    +
    + `; +} + function formatDuration(seconds) { const value = Number(seconds || 0); if (value < 60) return `${Math.max(0, value)}s`; @@ -415,6 +449,44 @@ function countryFromLanguage(language) { return match ? match[1].toUpperCase() : null; } +function countryFromTimezone(timezone) { + const zones = { + "Europe/Prague": "CZ", + "Europe/Bratislava": "SK", + "Europe/Warsaw": "PL", + "Europe/Berlin": "DE", + "Europe/Vienna": "AT", + "Europe/Budapest": "HU" + }; + return zones[String(timezone || "")] || null; +} + +function shortUrl(value) { + if (!value) return ""; + try { + const url = new URL(value); + const path = `${url.pathname}${url.search}`.replace(/\/$/, ""); + return `${url.hostname}${path || "/"}`; + } catch { + return String(value); + } +} + +function pageLabel(item) { + return item.title || shortUrl(item.url) || item.url || "stranka"; +} + +function formatVisitedAt(value) { + if (!value) return ""; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return ""; + return date.toLocaleTimeString("cs-CZ", { hour: "2-digit", minute: "2-digit" }); +} + +function deviceLabel(device = {}, language, timezone) { + return [device.platform, device.viewport, language, timezone].filter(Boolean).join(" | "); +} + function openPhotoPreview(url, name) { let modal = $("#photoModal"); if (!modal) { diff --git a/server.js b/server.js index 459a5cf..deb0f2e 100644 --- a/server.js +++ b/server.js @@ -570,7 +570,7 @@ function formatConversationSummary(row) { language: row.language, timezone: row.timezone, ip: row.ip, - countryCode: row.country_code, + countryCode: row.country_code || countryFromTimezone(row.timezone) || countryFromLanguage(row.language), vpnRisk: Boolean(row.vpn_risk), lastBody: row.last_body, messageCount: row.message_count, @@ -750,6 +750,23 @@ function countryFromHeaders(req) { return req.headers["cf-ipcountry"] || req.headers["x-vercel-ip-country"] || null; } +function countryFromTimezone(timezone) { + const zones = { + "Europe/Prague": "CZ", + "Europe/Bratislava": "SK", + "Europe/Warsaw": "PL", + "Europe/Berlin": "DE", + "Europe/Vienna": "AT", + "Europe/Budapest": "HU" + }; + return zones[String(timezone || "")] || null; +} + +function countryFromLanguage(language) { + const match = String(language || "").match(/-([A-Za-z]{2})\b/); + return match ? match[1].toUpperCase() : null; +} + function isAllowedImageAttachment(attachment, buffer) { const extension = path.extname(attachment.name || "").toLowerCase(); const type = String(attachment.type || "").toLowerCase();