mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-07-07 12:57:54 +02:00
improve campaign trendsline campaign box
Escape context in analytics graphs Fix login page dark mode Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
@@ -77,6 +77,8 @@
|
||||
let width = 300;
|
||||
let height = 240; // Increased height for better label spacing
|
||||
let containerReady = false;
|
||||
let tooltip;
|
||||
let tooltipTimeout;
|
||||
|
||||
// User controls for N
|
||||
let trendN = 4;
|
||||
@@ -729,108 +731,191 @@
|
||||
});
|
||||
}
|
||||
|
||||
function showTooltip(event, data, index, metricKey) {
|
||||
if (!tooltip || !data) return;
|
||||
|
||||
// clear any pending hide
|
||||
if (tooltipTimeout) clearTimeout(tooltipTimeout);
|
||||
|
||||
// show tooltip
|
||||
tooltip.style.display = 'block';
|
||||
|
||||
// use requestAnimationFrame for positioning similar to EventTimeline
|
||||
requestAnimationFrame(() => {
|
||||
// get cursor position relative to the chart container
|
||||
const containerRect = chartContainer.getBoundingClientRect();
|
||||
const x = event.clientX - containerRect.left;
|
||||
const y = event.clientY - containerRect.top;
|
||||
|
||||
const tooltipWidth = 250;
|
||||
const containerWidth = chartContainer.offsetWidth;
|
||||
const leftPosition = x + 10;
|
||||
const rightEdge = leftPosition + tooltipWidth;
|
||||
|
||||
// better positioning logic - more balanced positioning
|
||||
let adjustedLeft;
|
||||
if (rightEdge > containerWidth - 20) {
|
||||
// position to the left with some offset, not too far
|
||||
adjustedLeft = x - tooltipWidth + 40;
|
||||
} else {
|
||||
adjustedLeft = leftPosition;
|
||||
}
|
||||
|
||||
tooltip.style.left = `${Math.max(10, adjustedLeft)}px`;
|
||||
tooltip.style.top = `${Math.max(10, y - 10)}px`;
|
||||
|
||||
// update tooltip content
|
||||
updateTooltipContent(data, metricKey);
|
||||
});
|
||||
|
||||
// show value label for hovered metric
|
||||
const svg = chartContainer.querySelector('svg');
|
||||
const valueLabel = svg?.querySelector(`.value-label-${metricKey}-${index}`);
|
||||
if (valueLabel) {
|
||||
valueLabel.setAttribute('opacity', '1');
|
||||
}
|
||||
}
|
||||
|
||||
function hideTooltip(index, metricKey) {
|
||||
if (tooltipTimeout) clearTimeout(tooltipTimeout);
|
||||
tooltipTimeout = setTimeout(() => {
|
||||
if (tooltip) {
|
||||
tooltip.style.display = 'none';
|
||||
}
|
||||
}, 150);
|
||||
|
||||
// hide value label
|
||||
const svg = chartContainer.querySelector('svg');
|
||||
const valueLabel = svg?.querySelector(`.value-label-${metricKey}-${index}`);
|
||||
if (valueLabel) {
|
||||
valueLabel.setAttribute('opacity', '0');
|
||||
}
|
||||
}
|
||||
|
||||
function updateTooltipContent(data, hoveredMetric) {
|
||||
if (!tooltip) return;
|
||||
|
||||
try {
|
||||
const formattedDate =
|
||||
data.date instanceof Date
|
||||
? `${data.date.getFullYear()}/${String(data.date.getMonth() + 1).padStart(2, '0')}`
|
||||
: 'Unknown Date';
|
||||
|
||||
const visibleMetricsList = metrics.filter((metric) => visibleMetrics[metric.key]);
|
||||
const hoveredMetricInfo = metrics.find((metric) => metric.key === hoveredMetric);
|
||||
|
||||
// clear tooltip and build content safely using DOM methods
|
||||
tooltip.innerHTML = '';
|
||||
|
||||
// create main container
|
||||
const container = document.createElement('div');
|
||||
container.className = 'overflow-hidden';
|
||||
|
||||
// create header with colored border
|
||||
const header = document.createElement('div');
|
||||
header.className = 'border-t-4';
|
||||
header.style.borderTopColor = hoveredMetricInfo?.color || '#3b82f6';
|
||||
|
||||
const headerContent = document.createElement('div');
|
||||
headerContent.className = 'px-4 py-3 text-gray-800 dark:text-gray-200';
|
||||
|
||||
const headerFlex = document.createElement('div');
|
||||
headerFlex.className = 'flex items-center space-x-2';
|
||||
|
||||
// no indicator dot needed for cleaner look
|
||||
|
||||
const textContainer = document.createElement('div');
|
||||
textContainer.className = 'flex-1 min-w-0';
|
||||
|
||||
const title = document.createElement('h3');
|
||||
title.className = 'text-sm font-bold truncate';
|
||||
title.textContent = data.name;
|
||||
|
||||
const date = document.createElement('p');
|
||||
date.className = 'text-xs text-gray-600 dark:text-gray-400';
|
||||
date.textContent = formattedDate;
|
||||
|
||||
textContainer.appendChild(title);
|
||||
textContainer.appendChild(date);
|
||||
headerFlex.appendChild(textContainer);
|
||||
headerContent.appendChild(headerFlex);
|
||||
header.appendChild(headerContent);
|
||||
container.appendChild(header);
|
||||
|
||||
// create body content
|
||||
const body = document.createElement('div');
|
||||
body.className = 'px-4 py-3 space-y-3';
|
||||
|
||||
// recipients section
|
||||
const recipientsSection = document.createElement('div');
|
||||
recipientsSection.className = 'flex items-center space-x-2';
|
||||
|
||||
// no icon needed for cleaner look
|
||||
|
||||
const recipientsText = document.createElement('span');
|
||||
recipientsText.className = 'text-sm text-gray-700 dark:text-gray-300';
|
||||
recipientsText.textContent = `Total Recipients: ${data.totalRecipients || 0}`;
|
||||
|
||||
recipientsSection.appendChild(recipientsText);
|
||||
body.appendChild(recipientsSection);
|
||||
|
||||
// metrics section
|
||||
if (visibleMetricsList.length > 0) {
|
||||
const metricsSection = document.createElement('div');
|
||||
metricsSection.className =
|
||||
'mt-3 pt-3 border-t border-gray-200 dark:border-gray-600 space-y-2';
|
||||
|
||||
visibleMetricsList.forEach((metric) => {
|
||||
const metricRow = document.createElement('div');
|
||||
metricRow.className = 'flex items-center justify-between';
|
||||
|
||||
const metricLeft = document.createElement('div');
|
||||
metricLeft.className = 'flex items-center space-x-2';
|
||||
|
||||
const metricDot = document.createElement('div');
|
||||
metricDot.className = 'w-3 h-3 rounded-full';
|
||||
metricDot.style.backgroundColor = metric.color;
|
||||
|
||||
const metricLabel = document.createElement('span');
|
||||
metricLabel.className = 'text-sm text-gray-700 dark:text-gray-300';
|
||||
metricLabel.textContent = metric.label;
|
||||
|
||||
const metricValue = document.createElement('span');
|
||||
metricValue.className = 'text-sm font-semibold';
|
||||
metricValue.style.color = metric.color;
|
||||
metricValue.textContent = Math.round(data[metric.key] || 0).toString();
|
||||
|
||||
metricLeft.appendChild(metricDot);
|
||||
metricLeft.appendChild(metricLabel);
|
||||
metricRow.appendChild(metricLeft);
|
||||
metricRow.appendChild(metricValue);
|
||||
metricsSection.appendChild(metricRow);
|
||||
});
|
||||
|
||||
body.appendChild(metricsSection);
|
||||
}
|
||||
|
||||
container.appendChild(body);
|
||||
tooltip.appendChild(container);
|
||||
} catch (e) {
|
||||
console.error('Error updating tooltip content:', e);
|
||||
hideTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
function createTooltip(svg, points) {
|
||||
const tooltipGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
||||
tooltipGroup.setAttribute('class', 'tooltip-group');
|
||||
tooltipGroup.setAttribute('display', 'none');
|
||||
|
||||
const tooltipRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
||||
tooltipRect.setAttribute('rx', '4');
|
||||
tooltipRect.setAttribute(
|
||||
'fill',
|
||||
document.documentElement.classList.contains('dark') ? '#111827' : '#1F2937'
|
||||
);
|
||||
tooltipRect.setAttribute(
|
||||
'stroke',
|
||||
document.documentElement.classList.contains('dark') ? '#374151' : '#4b5563'
|
||||
);
|
||||
tooltipRect.setAttribute('opacity', '0.95');
|
||||
|
||||
const tooltipText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
tooltipText.setAttribute('fill', 'white');
|
||||
tooltipText.setAttribute('font-size', '12');
|
||||
tooltipText.setAttribute('font-weight', '500');
|
||||
|
||||
tooltipGroup.appendChild(tooltipRect);
|
||||
tooltipGroup.appendChild(tooltipText);
|
||||
svg.appendChild(tooltipGroup);
|
||||
|
||||
points.forEach((point) => {
|
||||
point.addEventListener('mouseenter', (e) => {
|
||||
const index = parseInt(e.target.getAttribute('data-index'));
|
||||
const metricKey = e.target.getAttribute('data-metric');
|
||||
const data = chartData[index];
|
||||
|
||||
// Show only the value label for the hovered metric
|
||||
const valueLabel = svg.querySelector(`.value-label-${metricKey}-${index}`);
|
||||
if (valueLabel) {
|
||||
valueLabel.setAttribute('opacity', '1');
|
||||
}
|
||||
|
||||
while (tooltipText.firstChild) tooltipText.removeChild(tooltipText.firstChild);
|
||||
|
||||
const labelSpan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
|
||||
labelSpan.setAttribute('x', '10');
|
||||
labelSpan.setAttribute('dy', '15');
|
||||
if (data.date instanceof Date) {
|
||||
labelSpan.textContent = `${data.date.getFullYear()}/${String(data.date.getMonth() + 1).padStart(2, '0')} ${data.name}`;
|
||||
} else {
|
||||
labelSpan.textContent = data.name;
|
||||
}
|
||||
tooltipText.appendChild(labelSpan);
|
||||
|
||||
metrics.forEach((metric, i) => {
|
||||
if (visibleMetrics[metric.key]) {
|
||||
const metricSpan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
|
||||
metricSpan.setAttribute('x', '10');
|
||||
metricSpan.setAttribute('dy', '15');
|
||||
metricSpan.setAttribute('fill', metric.color);
|
||||
metricSpan.textContent = `${metric.label}: ${Math.round(data[metric.key] || 0)}`;
|
||||
tooltipText.appendChild(metricSpan);
|
||||
}
|
||||
});
|
||||
|
||||
const recipientsSpan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
|
||||
recipientsSpan.setAttribute('x', '10');
|
||||
recipientsSpan.setAttribute('dy', '15');
|
||||
recipientsSpan.textContent = `Recipients: ${data.totalRecipients}`;
|
||||
tooltipText.appendChild(recipientsSpan);
|
||||
|
||||
const bbox = tooltipText.getBBox();
|
||||
tooltipRect.setAttribute('x', (bbox.x - 5).toString());
|
||||
tooltipRect.setAttribute('y', (bbox.y - 5).toString());
|
||||
tooltipRect.setAttribute('width', (bbox.width + 10).toString());
|
||||
tooltipRect.setAttribute('height', (bbox.height + 10).toString());
|
||||
|
||||
const svgRect = svg.getBoundingClientRect();
|
||||
const x = parseFloat(e.target.getAttribute('cx'));
|
||||
const y = parseFloat(e.target.getAttribute('cy'));
|
||||
const tooltipWidth = bbox.width + 10;
|
||||
let tooltipX = x + 10;
|
||||
// If tooltip would overflow right edge, show to the left
|
||||
if (tooltipX + tooltipWidth > width - 10) {
|
||||
tooltipX = x - tooltipWidth - 10;
|
||||
}
|
||||
// Prevent tooltip from being cut off at the top
|
||||
let tooltipY = y - 60;
|
||||
if (tooltipY < 0) {
|
||||
tooltipY = y + 10;
|
||||
}
|
||||
tooltipGroup.setAttribute('transform', `translate(${tooltipX}, ${tooltipY})`);
|
||||
tooltipGroup.setAttribute('display', 'block');
|
||||
showTooltip(e, data, index, metricKey);
|
||||
});
|
||||
|
||||
point.addEventListener('mouseleave', (e) => {
|
||||
const index = parseInt(e.target.getAttribute('data-index'));
|
||||
const metricKey = e.target.getAttribute('data-metric');
|
||||
|
||||
// Hide the value label for this metric
|
||||
const valueLabel = svg.querySelector(`.value-label-${metricKey}-${index}`);
|
||||
if (valueLabel) {
|
||||
valueLabel.setAttribute('opacity', '0');
|
||||
}
|
||||
|
||||
tooltipGroup.setAttribute('display', 'none');
|
||||
hideTooltip(index, metricKey);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -881,6 +966,9 @@
|
||||
if (pendingTimeout) {
|
||||
clearTimeout(pendingTimeout);
|
||||
}
|
||||
if (tooltipTimeout) {
|
||||
clearTimeout(tooltipTimeout);
|
||||
}
|
||||
});
|
||||
|
||||
// Only create chart when width is set and chartData is ready
|
||||
@@ -1056,11 +1144,17 @@
|
||||
<div class="mt-8 mb-6"></div>
|
||||
{#key chartKey}
|
||||
{#if containerReady}
|
||||
<div
|
||||
bind:this={chartContainer}
|
||||
class="min-h-[220px] max-h-[280px] w-full box-border relative rounded-md bg-white dark:bg-gray-900 m-1 transition-colors duration-200"
|
||||
style="contain: layout style;"
|
||||
></div>
|
||||
<div class="relative">
|
||||
<div
|
||||
bind:this={chartContainer}
|
||||
class="min-h-[220px] max-h-[280px] w-full box-border relative rounded-md bg-white dark:bg-gray-900 m-1 transition-colors duration-200"
|
||||
style="contain: layout style;"
|
||||
></div>
|
||||
<div
|
||||
bind:this={tooltip}
|
||||
class="absolute z-50 hidden bg-white dark:bg-gray-900 rounded-lg shadow-xl border border-gray-200 dark:border-gray-700/60 max-w-xs transition-colors duration-200"
|
||||
></div>
|
||||
</div>
|
||||
{/if}
|
||||
{/key}
|
||||
<!-- DEBUG: Show moving average arrays for openRate and submissionRate
|
||||
|
||||
@@ -554,103 +554,153 @@
|
||||
second: '2-digit'
|
||||
});
|
||||
|
||||
// use template literals for better performance
|
||||
const content = `
|
||||
<div class="overflow-hidden">
|
||||
<div class="border-t-4" style="border-top-color: ${eventColor};">
|
||||
<div class="px-4 py-3 text-gray-800 dark:text-gray-200">
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="flex-shrink-0 w-5 h-5" style="color: ${eventColor};">
|
||||
${eventIcon}
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-sm font-bold truncate">${eventInfo.name}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-4 py-3 space-y-3">
|
||||
${
|
||||
d.recipient?.email
|
||||
? `
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="flex-shrink-0 w-4 h-4 text-gray-500 dark:text-gray-400">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300 truncate">${d.recipient.email}</span>
|
||||
</div>
|
||||
`
|
||||
: ''
|
||||
}
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="flex-shrink-0 w-4 h-4 text-gray-500 dark:text-gray-400">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-sm text-gray-700 dark:text-gray-300">
|
||||
${formattedDate.split(',')[0]} ${formattedTime}
|
||||
</div>
|
||||
</div>
|
||||
${
|
||||
d.ip || d.userAgent
|
||||
? `
|
||||
<div class="mt-3 pt-3 border-t border-gray-200 dark:border-gray-600 space-y-2">
|
||||
${
|
||||
d.ip
|
||||
? `
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="flex-shrink-0 w-4 h-4 text-gray-500 dark:text-gray-400">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400 truncate">${d.ip}</span>
|
||||
</div>
|
||||
`
|
||||
: ''
|
||||
}
|
||||
${
|
||||
d.userAgent
|
||||
? `
|
||||
<div class="flex items-start space-x-2">
|
||||
<div class="flex-shrink-0 w-4 h-4 text-gray-500 dark:text-gray-400 mt-0.5">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400 break-words">${d.userAgent.length > 80 ? d.userAgent.substring(0, 80) + '...' : d.userAgent}</span>
|
||||
</div>
|
||||
`
|
||||
: ''
|
||||
}
|
||||
</div>
|
||||
`
|
||||
: ''
|
||||
}
|
||||
${
|
||||
d.data
|
||||
? `
|
||||
<div class="mt-3 pt-3 border-t border-gray-200 dark:border-gray-600">
|
||||
<div class="flex items-start space-x-2">
|
||||
<div class="flex-shrink-0 w-4 h-4 text-gray-500 dark:text-gray-400 mt-0.5">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400 break-words">${d.data.length > 100 ? d.data.substring(0, 100) + '...' : d.data}</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ''
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
tooltip.innerHTML = '';
|
||||
|
||||
tooltip.innerHTML = content;
|
||||
// create main container
|
||||
const container = document.createElement('div');
|
||||
container.className = 'overflow-hidden';
|
||||
|
||||
// create header with colored border
|
||||
const header = document.createElement('div');
|
||||
header.className = 'border-t-4';
|
||||
header.style.borderTopColor = eventColor;
|
||||
|
||||
const headerContent = document.createElement('div');
|
||||
headerContent.className = 'px-4 py-3 text-gray-800 dark:text-gray-200';
|
||||
|
||||
const headerFlex = document.createElement('div');
|
||||
headerFlex.className = 'flex items-center space-x-2';
|
||||
|
||||
// create icon container
|
||||
const iconContainer = document.createElement('div');
|
||||
iconContainer.className = 'flex-shrink-0 w-5 h-5';
|
||||
iconContainer.style.color = eventColor;
|
||||
iconContainer.innerHTML = eventIcon; // eventIcon is safe SVG from getEventIcon function
|
||||
|
||||
const textContainer = document.createElement('div');
|
||||
textContainer.className = 'flex-1 min-w-0';
|
||||
|
||||
const title = document.createElement('h3');
|
||||
title.className = 'text-sm font-bold truncate';
|
||||
title.textContent = eventInfo.name;
|
||||
|
||||
textContainer.appendChild(title);
|
||||
headerFlex.appendChild(iconContainer);
|
||||
headerFlex.appendChild(textContainer);
|
||||
headerContent.appendChild(headerFlex);
|
||||
header.appendChild(headerContent);
|
||||
container.appendChild(header);
|
||||
|
||||
// create body content
|
||||
const body = document.createElement('div');
|
||||
body.className = 'px-4 py-3 space-y-3';
|
||||
|
||||
// email section
|
||||
if (d.recipient?.email) {
|
||||
const emailSection = document.createElement('div');
|
||||
emailSection.className = 'flex items-center space-x-2';
|
||||
|
||||
const emailIcon = document.createElement('div');
|
||||
emailIcon.className = 'flex-shrink-0 w-4 h-4 text-gray-500 dark:text-gray-400';
|
||||
emailIcon.innerHTML =
|
||||
'<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207"/></svg>';
|
||||
|
||||
const emailText = document.createElement('span');
|
||||
emailText.className = 'text-sm text-gray-700 dark:text-gray-300 truncate';
|
||||
emailText.textContent = d.recipient.email;
|
||||
|
||||
emailSection.appendChild(emailIcon);
|
||||
emailSection.appendChild(emailText);
|
||||
body.appendChild(emailSection);
|
||||
}
|
||||
|
||||
// time section
|
||||
const timeSection = document.createElement('div');
|
||||
timeSection.className = 'flex items-center space-x-2';
|
||||
|
||||
const timeIcon = document.createElement('div');
|
||||
timeIcon.className = 'flex-shrink-0 w-4 h-4 text-gray-500 dark:text-gray-400';
|
||||
timeIcon.innerHTML =
|
||||
'<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>';
|
||||
|
||||
const timeText = document.createElement('div');
|
||||
timeText.className = 'text-sm text-gray-700 dark:text-gray-300';
|
||||
timeText.textContent = `${formattedDate.split(',')[0]} ${formattedTime}`;
|
||||
|
||||
timeSection.appendChild(timeIcon);
|
||||
timeSection.appendChild(timeText);
|
||||
body.appendChild(timeSection);
|
||||
|
||||
// ip/useragent section
|
||||
if (d.ip || d.userAgent) {
|
||||
const techSection = document.createElement('div');
|
||||
techSection.className = 'mt-3 pt-3 border-t border-gray-200 dark:border-gray-600 space-y-2';
|
||||
|
||||
if (d.ip) {
|
||||
const ipSection = document.createElement('div');
|
||||
ipSection.className = 'flex items-center space-x-2';
|
||||
|
||||
const ipIcon = document.createElement('div');
|
||||
ipIcon.className = 'flex-shrink-0 w-4 h-4 text-gray-500 dark:text-gray-400';
|
||||
ipIcon.innerHTML =
|
||||
'<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"/></svg>';
|
||||
|
||||
const ipText = document.createElement('span');
|
||||
ipText.className = 'text-xs text-gray-600 dark:text-gray-400 truncate';
|
||||
ipText.textContent = d.ip;
|
||||
|
||||
ipSection.appendChild(ipIcon);
|
||||
ipSection.appendChild(ipText);
|
||||
techSection.appendChild(ipSection);
|
||||
}
|
||||
|
||||
if (d.userAgent) {
|
||||
const uaSection = document.createElement('div');
|
||||
uaSection.className = 'flex items-start space-x-2';
|
||||
|
||||
const uaIcon = document.createElement('div');
|
||||
uaIcon.className = 'flex-shrink-0 w-4 h-4 text-gray-500 dark:text-gray-400 mt-0.5';
|
||||
uaIcon.innerHTML =
|
||||
'<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>';
|
||||
|
||||
const uaText = document.createElement('span');
|
||||
uaText.className = 'text-xs text-gray-600 dark:text-gray-400 break-words';
|
||||
uaText.textContent =
|
||||
d.userAgent.length > 80 ? d.userAgent.substring(0, 80) + '...' : d.userAgent;
|
||||
|
||||
uaSection.appendChild(uaIcon);
|
||||
uaSection.appendChild(uaText);
|
||||
techSection.appendChild(uaSection);
|
||||
}
|
||||
|
||||
body.appendChild(techSection);
|
||||
}
|
||||
|
||||
// data section
|
||||
if (d.data) {
|
||||
const dataSection = document.createElement('div');
|
||||
dataSection.className = 'mt-3 pt-3 border-t border-gray-200 dark:border-gray-600';
|
||||
|
||||
const dataFlex = document.createElement('div');
|
||||
dataFlex.className = 'flex items-start space-x-2';
|
||||
|
||||
const dataIcon = document.createElement('div');
|
||||
dataIcon.className = 'flex-shrink-0 w-4 h-4 text-gray-500 dark:text-gray-400 mt-0.5';
|
||||
dataIcon.innerHTML =
|
||||
'<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>';
|
||||
|
||||
const dataText = document.createElement('div');
|
||||
dataText.className = 'text-xs text-gray-600 dark:text-gray-400 break-words';
|
||||
dataText.textContent = d.data.length > 100 ? d.data.substring(0, 100) + '...' : d.data;
|
||||
|
||||
dataFlex.appendChild(dataIcon);
|
||||
dataFlex.appendChild(dataText);
|
||||
dataSection.appendChild(dataFlex);
|
||||
body.appendChild(dataSection);
|
||||
}
|
||||
|
||||
container.appendChild(body);
|
||||
tooltip.appendChild(container);
|
||||
} catch (e) {
|
||||
console.error('Error updating tooltip content:', e);
|
||||
hideTooltipOptimized();
|
||||
|
||||
@@ -214,7 +214,7 @@
|
||||
|
||||
<HeadTitle title="Sign in" />
|
||||
<main
|
||||
class="h-screen grid-cols-1 grid md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-2 2xl:grid-cols-2 bg-white dark:bg-gray-800 transition-colors duration-200"
|
||||
class="h-screen grid-cols-1 grid md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-2 2xl:grid-cols-2 bg-white dark:bg-gray-900 transition-colors duration-200"
|
||||
>
|
||||
<!-- theme toggle -->
|
||||
<div class="fixed top-3 right-6 z-50">
|
||||
@@ -252,12 +252,32 @@
|
||||
on:submit={(e) => onSubmitLogin('password', e)}
|
||||
class="flex flex-col items-center justify-center w-full md:p-px lg:p-4"
|
||||
>
|
||||
<Input
|
||||
fieldName={'Username'}
|
||||
type="text"
|
||||
bind:value={formValues.username}
|
||||
submitOnEnter
|
||||
/>
|
||||
<div class="flex flex-col w-full p-4 h-24">
|
||||
<label
|
||||
for="Username"
|
||||
class="text-md font-semibold font-titilium text-pc-darkblue dark:text-gray-300 transition-colors duration-200"
|
||||
>Username</label
|
||||
>
|
||||
<input
|
||||
required
|
||||
on:keyup={(event) => {
|
||||
const t = /** @type {HTMLInputElement} */ (event.target);
|
||||
formValues.username = t.value;
|
||||
}}
|
||||
on:keydown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
onSubmitLogin('password', null);
|
||||
}
|
||||
}}
|
||||
value={formValues.username}
|
||||
autocomplete="off"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
id="Username"
|
||||
name="Username"
|
||||
class="w-full p-2 rounded bg-pc-lightblue dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:placeholder-gray-400 focus:outline-none focus:ring-0 focus:border-cta-blue dark:focus:border-highlight-blue/80 focus:border-2 transition-colors duration-200"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col w-full p-4 h-24">
|
||||
<label
|
||||
for="Password"
|
||||
@@ -302,7 +322,7 @@
|
||||
type={inputType}
|
||||
id="Password"
|
||||
name="Password"
|
||||
class="relative w-full p-2 rounded bg-pc-lightblue dark:bg-gray-900/60 dark:border-gray-700/60 dark:text-gray-300 dark:placeholder-gray-500 focus:outline-none focus:ring-0 focus:border-cta-blue dark:focus:border-highlight-blue/80 focus:border-2 transition-colors duration-200"
|
||||
class="relative w-full p-2 rounded bg-pc-lightblue dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:placeholder-gray-400 focus:outline-none focus:ring-0 focus:border-cta-blue dark:focus:border-highlight-blue/80 focus:border-2 transition-colors duration-200"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user