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
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:
co-authored by
Claude Opus 4.6
parent
7104ac9c39
commit
10d5d70dd0
+162
-5
@@ -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: Monday–Friday, 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>
|
||||
|
||||
Reference in New Issue
Block a user