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>
|
</a>
|
||||||
</div>
|
</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="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="flex items-center gap-2" id="business-status" role="status" aria-live="polite" aria-atomic="true">
|
||||||
<div class="w-2 h-2 bg-emerald-400 rounded-full animate-pulse"></div>
|
<div id="status-dot" class="w-2 h-2 rounded-full"></div>
|
||||||
<span class="text-xs text-secondary-600 dark:text-secondary-300 font-medium">Open now</span>
|
<span id="status-label" class="text-xs font-medium"></span>
|
||||||
<span class="text-xs text-secondary-400 dark:text-secondary-500">· Closes at 6 PM</span>
|
<span id="status-detail" class="text-xs"></span>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
href="https://maps.app.goo.gl/9bZ5ez1pjbU74ATq5"
|
href="https://maps.app.goo.gl/9bZ5ez1pjbU74ATq5"
|
||||||
@@ -1024,3 +1024,160 @@ const trustStats = [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</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