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:
Ronni Skansing
2025-10-13 19:09:44 +02:00
parent 33358934d1
commit b538278fa2
3 changed files with 364 additions and 200 deletions
@@ -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
+146 -96
View File
@@ -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();
+28 -8
View File
@@ -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>