feat: dynamic business hours status on Contact page
Deploy to Production / Build & Verify (push) Failing after 5m45s
Ping Search Engines / Notify Search Engines (push) Successful in 5s
Deploy to Production / Pre-Deploy Tests (push) Has been skipped
Deploy to Production / Deploy to Railway (push) Has been skipped
Deploy to Production / Deploy to Render (push) Has been skipped
Deploy to Production / Deploy to VPS (PM2) (push) Has been skipped
Deploy to Production / Deploy to Fly.io (push) Has been skipped
Deploy to Production / Post-Deploy Verification (push) Has been skipped
Deploy to Production / Notify on Failure (push) Successful in 2s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
E2E Test Suite / Smoke Tests (P0) (push) Failing after 10m8s
E2E Test Suite / Form Interaction Tests (push) Failing after 12m35s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 12m14s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 9m55s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 11m24s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 16m2s
E2E Test Suite / Security Header Tests (push) Failing after 8m6s
E2E Test Suite / Test Report Summary (push) Failing after 4s
E2E Test Suite / Mobile Device Tests (push) Failing after 3h11m8s
Uptime Monitor / Health & Response Time (push) Successful in 3s
Uptime Monitor / SSL Certificate (push) Successful in 2s
Uptime Monitor / Send Alerts (push) Has been skipped
Uptime Monitor / Record Uptime Success (push) Successful in 1s

Replace hardcoded 'Open now' text with automatic Open/Closed status
based on user's timezone. Uses Intl API to detect timezone and compare
against business hours (Mon-Fri 10AM-6PM IST). Status updates every
60 seconds. Includes aria-live for accessibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
WorkRoot Agent
2026-04-13 04:08:20 +05:30
co-authored by Claude Opus 4.6
parent 7104ac9c39
commit 10d5d70dd0
+162 -5
View File
@@ -512,12 +512,12 @@ const trustStats = [
</a>
</div>
<!-- Map footer info -->
<!-- Map footer info - Dynamic business hours status -->
<div class="px-5 py-4 flex items-center justify-between border-t border-secondary-100 dark:border-secondary-700">
<div class="flex items-center gap-2">
<div class="w-2 h-2 bg-emerald-400 rounded-full animate-pulse"></div>
<span class="text-xs text-secondary-600 dark:text-secondary-300 font-medium">Open now</span>
<span class="text-xs text-secondary-400 dark:text-secondary-500">· Closes at 6 PM</span>
<div class="flex items-center gap-2" id="business-status" role="status" aria-live="polite" aria-atomic="true">
<div id="status-dot" class="w-2 h-2 rounded-full"></div>
<span id="status-label" class="text-xs font-medium"></span>
<span id="status-detail" class="text-xs"></span>
</div>
<a
href="https://maps.app.goo.gl/9bZ5ez1pjbU74ATq5"
@@ -1024,3 +1024,160 @@ const trustStats = [
}
}
</script>
<script>
/**
* Business Hours Status Utility
* Automatically detects user timezone and shows Open/Closed status
* Business hours: MondayFriday, 10:00 AM 6:00 PM IST (Asia/Kolkata)
*/
const BUSINESS_TIMEZONE = 'Asia/Kolkata';
const OPEN_HOUR = 10; // 10:00 AM
const CLOSE_HOUR = 18; // 6:00 PM
interface BusinessStatus {
isOpen: boolean;
label: string;
detail: string;
}
function getBusinessStatus(): BusinessStatus {
const now = new Date();
// Get current day and time in the business timezone (IST)
const options: Intl.DateTimeFormatOptions = {
timeZone: BUSINESS_TIMEZONE,
hour: 'numeric',
minute: 'numeric',
weekday: 'short',
hour12: false,
};
const parts = new Intl.DateTimeFormat('en-US', options).formatToParts(now);
const weekday = parts.find(p => p.type === 'weekday')?.value ?? '';
const hour = parseInt(parts.find(p => p.type === 'hour')?.value ?? '0', 10);
const minute = parseInt(parts.find(p => p.type === 'minute')?.value ?? '0', 10);
// Map weekday abbreviation to day number (Mon=1 ... Fri=5)
const dayMap: Record<string, number> = {
Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6,
};
const dayNum = dayMap[weekday] ?? 0;
const isWeekday = dayNum >= 1 && dayNum <= 5;
const currentMinutes = hour * 60 + minute;
const openMinutes = OPEN_HOUR * 60;
const closeMinutes = CLOSE_HOUR * 60;
const isWithinHours = currentMinutes >= openMinutes && currentMinutes < closeMinutes;
const isOpen = isWeekday && isWithinHours;
let detail = '';
if (isOpen) {
// Show closing time in user's local timezone
const closingIST = new Date(now);
// Calculate the closing time in IST, then convert
const istOffset = getTimezoneOffsetMinutes(BUSINESS_TIMEZONE, now);
const localOffset = now.getTimezoneOffset(); // negative for east of UTC
const diffMinutes = istOffset + localOffset;
const minutesUntilClose = closeMinutes - currentMinutes;
if (minutesUntilClose <= 60) {
detail = `· Closes in ${minutesUntilClose} min`;
} else {
// Format closing time in user's local time
const closingLocal = new Date(now.getTime() + (minutesUntilClose - diffMinutes + diffMinutes) * 60000);
// Simpler approach: calculate closing time in UTC then display locally
const nowInIST = currentMinutes;
const minsToClose = closeMinutes - nowInIST;
const closingTime = new Date(now.getTime() + minsToClose * 60000);
const localCloseStr = closingTime.toLocaleTimeString(undefined, {
hour: 'numeric',
minute: '2-digit',
hour12: true,
});
detail = `· Closes at ${localCloseStr}`;
}
} else {
// Calculate next opening time
let daysUntilOpen = 0;
let nextDay = dayNum;
if (isWeekday && currentMinutes < openMinutes) {
// Today but before opening
daysUntilOpen = 0;
} else {
// After hours or weekend — find next weekday
nextDay = (dayNum + 1) % 7;
daysUntilOpen = 1;
while (nextDay < 1 || nextDay > 5) {
nextDay = (nextDay + 1) % 7;
daysUntilOpen++;
}
}
if (daysUntilOpen === 0) {
const minsToOpen = openMinutes - currentMinutes;
if (minsToOpen <= 60) {
detail = `· Opens in ${minsToOpen} min`;
} else {
const openingTime = new Date(now.getTime() + minsToOpen * 60000);
const localOpenStr = openingTime.toLocaleTimeString(undefined, {
hour: 'numeric',
minute: '2-digit',
hour12: true,
});
detail = `· Opens at ${localOpenStr}`;
}
} else if (daysUntilOpen === 1) {
detail = '· Opens tomorrow at 10 AM IST';
} else {
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
detail = `· Opens ${dayNames[nextDay]} at 10 AM IST`;
}
}
return {
isOpen,
label: isOpen ? 'Open' : 'Closed',
detail,
};
}
/** Get timezone offset in minutes from UTC for a given IANA timezone */
function getTimezoneOffsetMinutes(tz: string, date: Date): number {
const utcStr = date.toLocaleString('en-US', { timeZone: 'UTC' });
const tzStr = date.toLocaleString('en-US', { timeZone: tz });
const utcDate = new Date(utcStr);
const tzDate = new Date(tzStr);
return (tzDate.getTime() - utcDate.getTime()) / 60000;
}
function updateBusinessStatus(): void {
const dot = document.getElementById('status-dot');
const label = document.getElementById('status-label');
const detailEl = document.getElementById('status-detail');
if (!dot || !label || !detailEl) return;
const status = getBusinessStatus();
if (status.isOpen) {
dot.className = 'w-2 h-2 rounded-full bg-emerald-400 animate-pulse';
label.className = 'text-xs font-medium text-emerald-600 dark:text-emerald-400';
label.textContent = 'Open';
} else {
dot.className = 'w-2 h-2 rounded-full bg-red-400';
label.className = 'text-xs font-medium text-red-600 dark:text-red-400';
label.textContent = 'Closed';
}
detailEl.textContent = status.detail;
detailEl.className = 'text-xs text-secondary-400 dark:text-secondary-500';
}
// Run on page load
updateBusinessStatus();
// Re-check every 60 seconds so status updates if user keeps the page open
setInterval(updateBusinessStatus, 60_000);
</script>