Old green (#10b981) on light green bg had only 2.42:1 contrast and old red (#ef4444) on light red bg had 3.44:1 — both failing WCAG AA 4.5:1. Updated to dark green text/border (#155724) on #f0fff4 (8.39:1) and dark red text/border (#721c24) on #fff5f5 (10.29:1). Added ✓/✗ icons as non-color validation indicators, dark mode variants, and missing valid/invalid icons on phone, subject, and message fields. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1247 lines
65 KiB
Plaintext
1247 lines
65 KiB
Plaintext
---
|
||
// Contact Page for WorkRoot IT Solutions LLP
|
||
// Enhanced: Professional form design, interactive map embed, contact cards, social links, scroll animations
|
||
|
||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||
import SEO from '../components/SEO.astro';
|
||
|
||
// Contact info data
|
||
const contactInfo = [
|
||
{
|
||
icon: 'location',
|
||
title: 'Visit Us',
|
||
details: ['At Post Rajapur, Shantinagar', 'Khatav, Maharashtra 415503'],
|
||
link: 'https://maps.app.goo.gl/9bZ5ez1pjbU74ATq5',
|
||
linkLabel: 'Get directions',
|
||
color: 'primary',
|
||
},
|
||
{
|
||
icon: 'phone',
|
||
title: 'Call Us',
|
||
details: ['+91 9561417403'],
|
||
link: 'tel:+919561417403',
|
||
linkLabel: 'Call now',
|
||
color: 'accent',
|
||
},
|
||
{
|
||
icon: 'email',
|
||
title: 'Email Us',
|
||
details: ['admin@workroot.in'],
|
||
link: 'mailto:admin@workroot.in',
|
||
linkLabel: 'Send email',
|
||
color: 'emerald',
|
||
},
|
||
{
|
||
icon: 'clock',
|
||
title: 'Business Hours',
|
||
details: ['Mon – Fri: 10:00 AM – 6:00 PM', 'Sat – Sun: Closed'],
|
||
link: null,
|
||
linkLabel: null,
|
||
color: 'violet',
|
||
},
|
||
];
|
||
|
||
// Social links
|
||
const socialLinks = [
|
||
{
|
||
name: 'LinkedIn',
|
||
href: 'https://www.linkedin.com/company/workroot',
|
||
icon: 'linkedin',
|
||
description: 'Connect professionally',
|
||
color: 'hover:bg-[#0077b5]',
|
||
},
|
||
{
|
||
name: 'Twitter / X',
|
||
href: 'https://x.com/GhanwatAjay',
|
||
icon: 'twitter',
|
||
description: 'Follow for updates',
|
||
color: 'hover:bg-[#000000]',
|
||
},
|
||
{
|
||
name: 'GitHub',
|
||
href: 'https://git.workroot.in/WorkRoot/',
|
||
icon: 'github',
|
||
description: 'See our open source',
|
||
color: 'hover:bg-[#333333]',
|
||
},
|
||
{
|
||
name: 'Facebook',
|
||
href: 'https://www.facebook.com/profile.php?id=61576503897897',
|
||
icon: 'facebook',
|
||
description: 'Follow us on Facebook',
|
||
color: 'hover:bg-[#1877f2]',
|
||
},
|
||
];
|
||
|
||
// Trust indicators
|
||
const trustStats = [
|
||
{ value: '20+', label: 'Projects Delivered' },
|
||
{ value: '100%', label: 'Client Satisfaction' },
|
||
{ value: '< 24h', label: 'Response Time' },
|
||
{ value: '3+', label: 'Years Experience' },
|
||
];
|
||
---
|
||
|
||
<BaseLayout
|
||
title="Contact Us — Get a Free Quote"
|
||
description="Start your project with WorkRoot IT Solutions LLP. 20+ projects delivered, 100% client satisfaction, response within 24 hours. Reach us at admin@workroot.in or call +91 9561417403."
|
||
>
|
||
<SEO
|
||
slot="head"
|
||
type="ContactPage"
|
||
breadcrumbs={[
|
||
{ name: 'Home', url: '/' },
|
||
{ name: 'Contact', url: '/contact' }
|
||
]}
|
||
/>
|
||
<!-- Prefetch Google Maps domains used for directions link and map embed on this page -->
|
||
<link slot="head" rel="dns-prefetch" href="https://maps.google.com" />
|
||
<link slot="head" rel="preconnect" href="https://maps.google.com" />
|
||
<link slot="head" rel="dns-prefetch" href="https://www.google.com" />
|
||
<link slot="head" rel="dns-prefetch" href="https://maps.googleapis.com" />
|
||
|
||
<!-- Hero Section -->
|
||
<section class="relative py-24 bg-hero-dark overflow-hidden">
|
||
<!-- Grid pattern overlay -->
|
||
<div class="absolute inset-0 bg-grid-pattern bg-grid-80 opacity-30 pointer-events-none" aria-hidden="true"></div>
|
||
|
||
<!-- Decorative blobs -->
|
||
<div class="absolute -top-40 -right-32 w-96 h-96 bg-primary/15 rounded-full blur-2xl md:blur-3xl pointer-events-none" aria-hidden="true"></div>
|
||
<div class="absolute -bottom-32 -left-32 w-80 h-80 bg-accent/10 rounded-full blur-2xl md:blur-3xl pointer-events-none" aria-hidden="true"></div>
|
||
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] bg-primary/5 rounded-full blur-2xl md:blur-3xl pointer-events-none" aria-hidden="true"></div>
|
||
|
||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||
<div class="max-w-3xl mx-auto text-center">
|
||
<!-- Eyebrow label -->
|
||
<div class="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-primary/10 border border-primary/20 text-primary-300 text-sm font-medium mb-6 reveal-on-scroll">
|
||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
||
</svg>
|
||
Let's Start a Conversation
|
||
</div>
|
||
|
||
<h1 class="text-display-lg font-bold text-white mb-6 reveal-on-scroll reveal-delay-1">
|
||
We'd Love to<br>
|
||
<span class="text-transparent bg-clip-text bg-gradient-to-r from-primary-300 to-primary-500">Hear From You</span>
|
||
</h1>
|
||
<p class="text-xl text-secondary-300 max-w-2xl mx-auto reveal-on-scroll reveal-delay-2">
|
||
Whether you have a project in mind, a question about our services, or just want to explore the possibilities — our team is ready to help.
|
||
</p>
|
||
|
||
<!-- Trust stats strip -->
|
||
<div class="mt-12 grid grid-cols-2 sm:grid-cols-4 gap-4 reveal-on-scroll reveal-delay-3">
|
||
{trustStats.map((stat) => (
|
||
<div class="bg-white/5 border border-white/10 rounded-xl px-4 py-4 text-center backdrop-blur-sm">
|
||
<div class="text-2xl font-bold text-white mb-1">{stat.value}</div>
|
||
<div class="text-xs text-secondary-400 uppercase tracking-wider">{stat.label}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Main Contact Section -->
|
||
<section class="py-20 lg:py-28 bg-secondary-50 dark:bg-secondary-900">
|
||
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
|
||
<div class="grid lg:grid-cols-5 gap-12 xl:gap-16">
|
||
|
||
<!-- LEFT: Contact Form (3 cols) -->
|
||
<div class="lg:col-span-3 reveal-on-scroll">
|
||
<div class="bg-white dark:bg-secondary-800 rounded-2xl shadow-card border border-secondary-100 dark:border-secondary-700 p-8 lg:p-10">
|
||
<!-- Form Header -->
|
||
<div class="mb-8">
|
||
<h2 class="text-2xl font-bold text-secondary-900 dark:text-secondary-100 mb-2">Send Us a Message</h2>
|
||
<p class="text-secondary-500 dark:text-secondary-400 text-sm">Fill out the form below and we'll respond within 24 hours.</p>
|
||
</div>
|
||
|
||
<form id="contact-form" class="space-y-5" novalidate>
|
||
<!-- Honeypot field (anti-spam) - hidden from users -->
|
||
<div class="absolute -left-[9999px]" aria-hidden="true">
|
||
<label for="website">
|
||
Website
|
||
<input
|
||
type="text"
|
||
id="website"
|
||
name="website"
|
||
tabindex="-1"
|
||
autocomplete="off"
|
||
/>
|
||
</label>
|
||
</div>
|
||
|
||
<!-- Name + Email row -->
|
||
<div class="grid sm:grid-cols-2 gap-5">
|
||
<!-- Name Field -->
|
||
<div class="form-group">
|
||
<label for="name" class="block text-sm font-semibold text-secondary-700 dark:text-secondary-300 mb-1.5">
|
||
Full Name <span class="text-red-500" aria-hidden="true">*</span>
|
||
<span class="sr-only">(required)</span>
|
||
</label>
|
||
<div class="relative">
|
||
<div class="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none" aria-hidden="true">
|
||
<svg class="w-4 h-4 text-secondary-400 field-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||
</svg>
|
||
</div>
|
||
<input
|
||
type="text"
|
||
id="name"
|
||
name="name"
|
||
required
|
||
minlength="2"
|
||
autocomplete="name"
|
||
aria-required="true"
|
||
aria-describedby="name-error"
|
||
class="form-input w-full pl-10 pr-4 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 dark:text-secondary-100 placeholder-secondary-400 dark:placeholder-secondary-500 text-sm bg-secondary-50 dark:bg-secondary-700 focus:bg-white dark:focus:bg-secondary-700"
|
||
placeholder="John Doe"
|
||
/>
|
||
<div class="valid-check absolute inset-y-0 right-3 flex items-center hidden" aria-label="Valid">
|
||
<svg class="w-4 h-4 text-green-700 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" />
|
||
</svg>
|
||
</div>
|
||
<div class="invalid-check absolute inset-y-0 right-3 flex items-center hidden" aria-label="Invalid">
|
||
<svg class="w-4 h-4 text-red-800 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" />
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
<p class="mt-1.5 text-xs text-red-500 hidden flex items-center gap-1" id="name-error" data-error="name" role="alert">
|
||
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
|
||
Please enter your full name (at least 2 characters).
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Email Field -->
|
||
<div class="form-group">
|
||
<label for="email" class="block text-sm font-semibold text-secondary-700 dark:text-secondary-300 mb-1.5">
|
||
Email Address <span class="text-red-500" aria-hidden="true">*</span>
|
||
<span class="sr-only">(required)</span>
|
||
</label>
|
||
<div class="relative">
|
||
<div class="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none" aria-hidden="true">
|
||
<svg class="w-4 h-4 text-secondary-400 field-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||
</svg>
|
||
</div>
|
||
<input
|
||
type="email"
|
||
id="email"
|
||
name="email"
|
||
required
|
||
autocomplete="email"
|
||
aria-required="true"
|
||
aria-describedby="email-error"
|
||
class="form-input w-full pl-10 pr-4 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 dark:text-secondary-100 placeholder-secondary-400 dark:placeholder-secondary-500 text-sm bg-secondary-50 dark:bg-secondary-700 focus:bg-white dark:focus:bg-secondary-700"
|
||
placeholder="john@example.com"
|
||
/>
|
||
<div class="valid-check absolute inset-y-0 right-3 flex items-center hidden" aria-label="Valid">
|
||
<svg class="w-4 h-4 text-green-700 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" />
|
||
</svg>
|
||
</div>
|
||
<div class="invalid-check absolute inset-y-0 right-3 flex items-center hidden" aria-label="Invalid">
|
||
<svg class="w-4 h-4 text-red-800 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" />
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
<p class="mt-1.5 text-xs text-red-500 hidden flex items-center gap-1" id="email-error" data-error="email" role="alert">
|
||
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
|
||
Please enter a valid email address.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Phone + Subject row -->
|
||
<div class="grid sm:grid-cols-2 gap-5">
|
||
<!-- Phone Field -->
|
||
<div class="form-group">
|
||
<label for="phone" class="block text-sm font-semibold text-secondary-700 dark:text-secondary-300 mb-1.5">
|
||
Phone Number
|
||
<span class="text-secondary-400 font-normal">(optional)</span>
|
||
</label>
|
||
<div class="relative">
|
||
<div class="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none" aria-hidden="true">
|
||
<svg class="w-4 h-4 text-secondary-400 field-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
||
</svg>
|
||
</div>
|
||
<input
|
||
type="tel"
|
||
id="phone"
|
||
name="phone"
|
||
autocomplete="tel"
|
||
aria-describedby="phone-error"
|
||
class="form-input w-full pl-10 pr-4 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 dark:text-secondary-100 placeholder-secondary-400 dark:placeholder-secondary-500 text-sm bg-secondary-50 dark:bg-secondary-700 focus:bg-white dark:focus:bg-secondary-700"
|
||
placeholder="+1 (555) 123-4567"
|
||
/>
|
||
<div class="valid-check absolute inset-y-0 right-3 flex items-center hidden" aria-label="Valid">
|
||
<svg class="w-4 h-4 text-green-700 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" />
|
||
</svg>
|
||
</div>
|
||
<div class="invalid-check absolute inset-y-0 right-3 flex items-center hidden" aria-label="Invalid">
|
||
<svg class="w-4 h-4 text-red-800 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" />
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
<p class="mt-1.5 text-xs text-red-500 hidden flex items-center gap-1" id="phone-error" data-error="phone" role="alert">
|
||
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
|
||
Please enter a valid phone number.
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Subject Field -->
|
||
<div class="form-group">
|
||
<label for="subject" class="block text-sm font-semibold text-secondary-700 dark:text-secondary-300 mb-1.5">
|
||
Service Needed <span class="text-red-500" aria-hidden="true">*</span>
|
||
<span class="sr-only">(required)</span>
|
||
</label>
|
||
<div class="relative">
|
||
<div class="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none" aria-hidden="true">
|
||
<svg class="w-4 h-4 text-secondary-400 field-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||
</svg>
|
||
</div>
|
||
<select
|
||
id="subject"
|
||
name="subject"
|
||
required
|
||
aria-required="true"
|
||
aria-describedby="subject-error"
|
||
class="form-input w-full pl-10 pr-8 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 dark:text-secondary-100 text-sm bg-secondary-50 dark:bg-secondary-700 focus:bg-white dark:focus:bg-secondary-700 appearance-none cursor-pointer"
|
||
>
|
||
<option value="">Select a service...</option>
|
||
<option value="web-development">Web Development</option>
|
||
<option value="mobile-development">Mobile Development</option>
|
||
<option value="cloud-services">Cloud Services</option>
|
||
<option value="ai-ml">AI & Machine Learning</option>
|
||
<option value="consulting">IT Consulting</option>
|
||
<option value="support">Technical Support</option>
|
||
<option value="other">Other / General Inquiry</option>
|
||
</select>
|
||
<div class="valid-check absolute inset-y-0 right-9 flex items-center hidden" aria-label="Valid">
|
||
<svg class="w-4 h-4 text-green-700 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" />
|
||
</svg>
|
||
</div>
|
||
<div class="invalid-check absolute inset-y-0 right-9 flex items-center hidden" aria-label="Invalid">
|
||
<svg class="w-4 h-4 text-red-800 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" />
|
||
</svg>
|
||
</div>
|
||
<div class="absolute inset-y-0 right-3 flex items-center pointer-events-none" aria-hidden="true">
|
||
<svg class="w-4 h-4 text-secondary-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
<p class="mt-1.5 text-xs text-red-500 hidden flex items-center gap-1" id="subject-error" data-error="subject" role="alert">
|
||
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
|
||
Please select a service.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Budget range (new field) -->
|
||
<div class="form-group">
|
||
<label class="block text-sm font-semibold text-secondary-700 dark:text-secondary-300 mb-2">
|
||
Project Budget
|
||
<span class="text-secondary-400 dark:text-secondary-500 font-normal">(optional)</span>
|
||
</label>
|
||
<div class="flex flex-wrap gap-2" role="group" aria-label="Budget range options">
|
||
{['< $5K', '$5K – $15K', '$15K – $50K', '$50K+', 'Not sure yet'].map((range, i) => (
|
||
<label class="budget-option flex items-center gap-1.5 px-3 py-2 rounded-lg border border-secondary-200 dark:border-secondary-600 text-sm text-secondary-600 dark:text-secondary-400 cursor-pointer hover:border-primary hover:bg-primary/5 dark:hover:bg-primary/10 transition-all duration-150 has-[:checked]:border-primary has-[:checked]:bg-primary/10 has-[:checked]:text-primary-700 dark:has-[:checked]:text-primary-300">
|
||
<input type="radio" name="budget" value={range} class="sr-only" />
|
||
{range}
|
||
</label>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Message Field -->
|
||
<div class="form-group">
|
||
<div class="flex items-center justify-between mb-1.5">
|
||
<label for="message" class="block text-sm font-semibold text-secondary-700 dark:text-secondary-300">
|
||
Your Message <span class="text-red-500" aria-hidden="true">*</span>
|
||
<span class="sr-only">(required)</span>
|
||
</label>
|
||
<span id="char-count" class="text-xs text-secondary-400" aria-hidden="true">0 / 5000</span>
|
||
</div>
|
||
<div class="relative">
|
||
<textarea
|
||
id="message"
|
||
name="message"
|
||
required
|
||
minlength="10"
|
||
maxlength="5000"
|
||
rows="5"
|
||
aria-required="true"
|
||
aria-describedby="message-error message-hint"
|
||
class="form-input w-full px-4 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 dark:text-secondary-100 placeholder-secondary-400 dark:placeholder-secondary-500 text-sm bg-secondary-50 dark:bg-secondary-700 focus:bg-white dark:focus:bg-secondary-700 resize-none"
|
||
placeholder="Tell us about your project, goals, and timeline..."
|
||
></textarea>
|
||
<div class="valid-check absolute top-3 right-3 flex items-center hidden" aria-label="Valid">
|
||
<svg class="w-4 h-4 text-green-700 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" />
|
||
</svg>
|
||
</div>
|
||
<div class="invalid-check absolute top-3 right-3 flex items-center hidden" aria-label="Invalid">
|
||
<svg class="w-4 h-4 text-red-800 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" />
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
<p id="message-hint" class="mt-1 text-xs text-secondary-400">Minimum 10 characters. Be as detailed as possible for a faster, more accurate response.</p>
|
||
<p class="mt-1.5 text-xs text-red-500 hidden flex items-center gap-1" id="message-error" data-error="message" role="alert">
|
||
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
|
||
Please enter a message (at least 10 characters).
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Submit Button + Privacy note -->
|
||
<div class="pt-2">
|
||
<button
|
||
type="submit"
|
||
id="submit-btn"
|
||
class="w-full py-4 px-6 bg-gradient-to-r from-primary to-primary-600 text-white font-semibold rounded-xl hover:from-primary-600 hover:to-primary-700 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 transition-all duration-200 flex items-center justify-center gap-2 shadow-primary-glow/50 disabled:opacity-60 disabled:cursor-not-allowed group"
|
||
>
|
||
<span id="btn-text">Send Message</span>
|
||
<svg id="btn-arrow" class="w-5 h-5 group-hover:translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
|
||
</svg>
|
||
<svg id="btn-spinner" class="animate-spin w-5 h-5 hidden" fill="none" viewBox="0 0 24 24" aria-hidden="true">
|
||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||
</svg>
|
||
</button>
|
||
|
||
<!-- Privacy note -->
|
||
<p class="mt-3 text-center text-xs text-secondary-400">
|
||
<svg class="w-3.5 h-3.5 inline mr-1 text-secondary-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||
</svg>
|
||
We respect your privacy. Your information is never shared with third parties.
|
||
</p>
|
||
</div>
|
||
</form>
|
||
|
||
<!-- Success state (hidden by default) -->
|
||
<div id="success-state" class="hidden text-center py-12">
|
||
<div class="w-20 h-20 bg-emerald-100 dark:bg-emerald-900/30 rounded-full flex items-center justify-center mx-auto mb-6">
|
||
<svg class="w-10 h-10 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||
</svg>
|
||
</div>
|
||
<h3 class="text-2xl font-bold text-secondary-900 dark:text-secondary-100 mb-2">Message Sent!</h3>
|
||
<p class="text-secondary-600 dark:text-secondary-400 mb-6">Thank you for reaching out. We'll get back to you within 24 hours.</p>
|
||
<button
|
||
id="send-another"
|
||
class="inline-flex items-center gap-2 text-primary hover:text-primary-700 font-medium text-sm transition-colors"
|
||
>
|
||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||
</svg>
|
||
Send another message
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- RIGHT: Contact Info + Map + Social (2 cols) -->
|
||
<div class="lg:col-span-2 space-y-6">
|
||
|
||
<!-- Contact Info Cards -->
|
||
<div class="grid grid-cols-1 xs:grid-cols-2 gap-3 sm:gap-4 reveal-on-scroll reveal-delay-1">
|
||
{contactInfo.map((info) => (
|
||
<div class={`contact-card group relative bg-white dark:bg-secondary-800 rounded-2xl p-5 shadow-card border border-secondary-100 dark:border-secondary-700 hover:shadow-card-hover hover:-translate-y-1 transition-all duration-300 overflow-hidden`}>
|
||
<!-- Colored top accent bar -->
|
||
<div class={`absolute top-0 left-0 right-0 h-1 rounded-t-2xl ${
|
||
info.color === 'primary' ? 'bg-gradient-to-r from-primary to-primary-600' :
|
||
info.color === 'accent' ? 'bg-gradient-to-r from-accent to-accent-600' :
|
||
info.color === 'emerald' ? 'bg-gradient-to-r from-emerald-400 to-emerald-600' :
|
||
'bg-gradient-to-r from-violet-400 to-violet-600'
|
||
}`} aria-hidden="true"></div>
|
||
|
||
<!-- Icon -->
|
||
<div class={`w-10 h-10 rounded-xl flex items-center justify-center mb-3 ${
|
||
info.color === 'primary' ? 'bg-primary-50' :
|
||
info.color === 'accent' ? 'bg-amber-50' :
|
||
info.color === 'emerald' ? 'bg-emerald-50' :
|
||
'bg-violet-50'
|
||
}`} aria-hidden="true">
|
||
{info.icon === 'location' && (
|
||
<svg class="w-5 h-5 text-primary-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||
</svg>
|
||
)}
|
||
{info.icon === 'phone' && (
|
||
<svg class="w-5 h-5 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
||
</svg>
|
||
)}
|
||
{info.icon === 'email' && (
|
||
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||
</svg>
|
||
)}
|
||
{info.icon === 'clock' && (
|
||
<svg class="w-5 h-5 text-violet-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||
<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>
|
||
|
||
<h3 class="text-sm font-bold text-secondary-900 dark:text-secondary-100 mb-1.5">{info.title}</h3>
|
||
{info.details.map((detail) => (
|
||
<p class="text-xs text-secondary-500 dark:text-secondary-400 leading-relaxed">{detail}</p>
|
||
))}
|
||
{info.link && (
|
||
<a
|
||
href={info.link}
|
||
class={`mt-3 inline-flex items-center gap-1 text-xs font-medium transition-colors ${
|
||
info.color === 'primary' ? 'text-primary-600 hover:text-primary-700' :
|
||
info.color === 'accent' ? 'text-amber-600 hover:text-amber-700' :
|
||
info.color === 'emerald' ? 'text-emerald-600 hover:text-emerald-700' :
|
||
'text-violet-600 hover:text-violet-700'
|
||
}`}
|
||
{...(info.href?.startsWith('http') ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
|
||
>
|
||
{info.linkLabel}
|
||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||
</svg>
|
||
</a>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<!-- Map Embed -->
|
||
<div class="reveal-on-scroll reveal-delay-2">
|
||
<div class="bg-white dark:bg-secondary-800 rounded-2xl shadow-card border border-secondary-100 dark:border-secondary-700 overflow-hidden">
|
||
<div class="relative h-52 bg-secondary-100 dark:bg-secondary-700 group">
|
||
<!-- Google Maps Embed -->
|
||
<iframe
|
||
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3820.123456789!2d74.3309312!3d17.7733632!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x0%3A0x0!2zMTfCsDQ2JzI0LjEiTiA3NMKwMTknNTEuNCJF!5e0!3m2!1sen!2sin!4v1234567890"
|
||
width="100%"
|
||
height="450"
|
||
style="border:0;"
|
||
allowfullscreen=""
|
||
loading="lazy"
|
||
referrerpolicy="no-referrer-when-downgrade"
|
||
title="WorkRoot IT Solutions LLP Office Location"
|
||
class="w-full rounded-2xl absolute inset-0 h-full"
|
||
></iframe>
|
||
|
||
<!-- Hover overlay: Get Directions -->
|
||
<a
|
||
href="https://maps.app.goo.gl/9bZ5ez1pjbU74ATq5"
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
aria-label="Get directions to At Post Rajapur, Shantinagar, Khatav, Maharashtra 415503 (opens in new tab)"
|
||
class="absolute inset-0 bg-primary/90 flex flex-col items-center justify-center gap-2 opacity-0 group-hover:opacity-100 focus:opacity-100 transition-opacity duration-300"
|
||
>
|
||
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l5.447 2.724A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />
|
||
</svg>
|
||
<span class="text-white font-semibold text-sm">Get Directions</span>
|
||
<span class="text-white/70 text-xs">Opens in Google Maps</span>
|
||
</a>
|
||
</div>
|
||
|
||
<!-- 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" 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"
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
class="text-xs text-primary-600 hover:text-primary-700 font-medium flex items-center gap-1 transition-colors"
|
||
aria-label="View on map (opens in new tab)"
|
||
>
|
||
View on map
|
||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||
</svg>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Social Links -->
|
||
<div class="bg-secondary-900 rounded-2xl p-6 reveal-on-scroll reveal-delay-3">
|
||
<h3 class="text-base font-bold text-white mb-1">Connect With Us</h3>
|
||
<p class="text-secondary-400 text-sm mb-5">Follow us for the latest updates and insights.</p>
|
||
<div class="space-y-3">
|
||
{socialLinks.map((social) => (
|
||
<a
|
||
href={social.href}
|
||
aria-label={`${social.name} – ${social.description}`}
|
||
class={`flex items-center gap-3 p-3 rounded-xl bg-secondary-800 text-secondary-300 hover:text-white transition-all duration-200 group ${social.color}`}
|
||
>
|
||
<div class="w-8 h-8 flex items-center justify-center flex-shrink-0" aria-hidden="true">
|
||
{social.icon === 'linkedin' && (
|
||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||
<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"/>
|
||
</svg>
|
||
)}
|
||
{social.icon === 'twitter' && (
|
||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
|
||
</svg>
|
||
)}
|
||
{social.icon === 'github' && (
|
||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
||
</svg>
|
||
)}
|
||
{social.icon === 'facebook' && (
|
||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
|
||
</svg>
|
||
)}
|
||
</div>
|
||
<div class="min-w-0">
|
||
<div class="text-sm font-medium leading-none mb-0.5">{social.name}</div>
|
||
<div class="text-xs text-secondary-500 group-hover:text-secondary-300 transition-colors">{social.description}</div>
|
||
</div>
|
||
<svg class="w-4 h-4 ml-auto flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||
</svg>
|
||
</a>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- FAQ Strip -->
|
||
<section class="py-16 bg-white dark:bg-secondary-900 border-t border-secondary-100 dark:border-secondary-800">
|
||
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
|
||
<div class="max-w-2xl mx-auto text-center mb-10 reveal-on-scroll">
|
||
<h2 class="text-2xl font-bold text-secondary-900 dark:text-secondary-100 mb-3">Common Questions</h2>
|
||
<p class="text-secondary-500 dark:text-secondary-400 text-sm">Quick answers to help you before reaching out.</p>
|
||
</div>
|
||
<div class="max-w-3xl mx-auto space-y-3">
|
||
{[
|
||
{
|
||
q: 'How quickly will you respond to my inquiry?',
|
||
a: 'We respond to all inquiries within 24 hours on business days. For urgent matters, please call us directly.',
|
||
},
|
||
{
|
||
q: 'Do you work with clients outside Maharashtra?',
|
||
a: 'Absolutely. We work with clients across India and globally, and are fully equipped for remote collaboration across time zones.',
|
||
},
|
||
{
|
||
q: 'What information should I include in my message?',
|
||
a: 'The more detail, the better! Include your project goals, timeline, budget range, and any technical requirements you already have in mind.',
|
||
},
|
||
{
|
||
q: 'Do you offer free consultations?',
|
||
a: 'Yes — we offer a complimentary 30-minute discovery call for all new project inquiries. Just mention it in your message.',
|
||
},
|
||
].map((faq, i) => (
|
||
<details
|
||
class={`faq-item group bg-secondary-50 dark:bg-secondary-800 rounded-xl border border-secondary-100 dark:border-secondary-700 overflow-hidden reveal-on-scroll reveal-delay-${Math.min(i + 1, 4)}`}
|
||
>
|
||
<summary class="flex items-center justify-between gap-4 px-6 py-4 cursor-pointer list-none hover:bg-secondary-100 dark:hover:bg-secondary-700 transition-colors duration-150">
|
||
<span class="font-semibold text-secondary-900 dark:text-secondary-100 text-sm">{faq.q}</span>
|
||
<div class="flex-shrink-0 w-6 h-6 rounded-full bg-secondary-200 dark:bg-secondary-700 group-open:bg-primary/10 flex items-center justify-center transition-colors" aria-hidden="true">
|
||
<svg class="w-3.5 h-3.5 text-secondary-600 group-open:text-primary group-open:rotate-45 transition-all duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 4v16m8-8H4" />
|
||
</svg>
|
||
</div>
|
||
</summary>
|
||
<div class="px-6 pb-4">
|
||
<p class="text-secondary-600 dark:text-secondary-400 text-sm leading-relaxed">{faq.a}</p>
|
||
</div>
|
||
</details>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- CTA Banner -->
|
||
<section class="py-16 bg-gradient-to-r from-secondary-900 via-secondary-800 to-secondary-900 relative overflow-hidden">
|
||
<div class="absolute inset-0 bg-grid-pattern bg-grid-64 opacity-20 pointer-events-none" aria-hidden="true"></div>
|
||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||
<div class="max-w-3xl mx-auto text-center reveal-on-scroll">
|
||
<p class="text-primary-300 text-sm font-medium uppercase tracking-widest mb-4">Ready to get started?</p>
|
||
<h2 class="text-3xl sm:text-4xl font-bold text-white mb-4">
|
||
Let's Build Something <span class="text-transparent bg-clip-text bg-gradient-to-r from-primary-300 to-accent-300">Remarkable</span> Together
|
||
</h2>
|
||
<p class="text-secondary-400 mb-8 max-w-xl mx-auto">
|
||
Join 200+ companies who've transformed their digital presence with WorkRoot.
|
||
</p>
|
||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||
<a
|
||
href="#contact-form"
|
||
class="inline-flex items-center justify-center gap-2 px-8 py-4 bg-primary text-white font-semibold rounded-xl hover:bg-primary-600 transition-colors duration-200 shadow-primary-glow focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 focus:ring-offset-secondary-900"
|
||
>
|
||
Start a Project
|
||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
|
||
</svg>
|
||
</a>
|
||
<a
|
||
href="tel:+919561417403"
|
||
class="inline-flex items-center justify-center gap-2 px-8 py-4 bg-white/10 text-white font-semibold rounded-xl hover:bg-white/20 transition-colors duration-200 border border-white/20 focus:outline-none focus:ring-2 focus:ring-white/50 focus:ring-offset-2 focus:ring-offset-secondary-900"
|
||
>
|
||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
||
</svg>
|
||
Call Us Directly
|
||
</a>
|
||
</div>
|
||
<!-- Response guarantee -->
|
||
<p class="mt-6 text-secondary-500 text-xs flex items-center justify-center gap-2">
|
||
<svg class="w-4 h-4 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||
</svg>
|
||
Guaranteed response within 24 business hours
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Toast Container -->
|
||
<div id="toast-container" class="fixed bottom-4 right-4 z-50 flex flex-col gap-3 max-w-sm w-full pointer-events-none" aria-live="assertive" aria-atomic="true"></div>
|
||
</BaseLayout>
|
||
|
||
<style>
|
||
/* Scroll-reveal base state */
|
||
.reveal-on-scroll {
|
||
opacity: 0;
|
||
transform: translateY(24px);
|
||
transition: opacity 0.6s ease, transform 0.6s ease;
|
||
}
|
||
.reveal-on-scroll.revealed {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
|
||
/* Staggered delays */
|
||
.reveal-delay-1 { transition-delay: 0.1s; }
|
||
.reveal-delay-2 { transition-delay: 0.2s; }
|
||
.reveal-delay-3 { transition-delay: 0.3s; }
|
||
.reveal-delay-4 { transition-delay: 0.4s; }
|
||
|
||
/* Respect reduced motion */
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.reveal-on-scroll {
|
||
opacity: 1;
|
||
transform: none;
|
||
transition: none;
|
||
}
|
||
.animate-ping,
|
||
.animate-float,
|
||
.animate-pulse {
|
||
animation: none;
|
||
}
|
||
}
|
||
|
||
/* Form input states — WCAG AA compliant (≥ 4.5:1 contrast) */
|
||
.form-input.is-valid {
|
||
border-color: #155724;
|
||
background-color: #f0fff4;
|
||
color: #155724;
|
||
}
|
||
.form-input.is-invalid {
|
||
border-color: #721c24;
|
||
background-color: #fff5f5;
|
||
color: #721c24;
|
||
}
|
||
/* Dark mode validation states */
|
||
:global(.dark) .form-input.is-valid {
|
||
border-color: #48bb78;
|
||
background-color: rgba(72, 187, 120, 0.1);
|
||
color: #c6f6d5;
|
||
}
|
||
:global(.dark) .form-input.is-invalid {
|
||
border-color: #fc8181;
|
||
background-color: rgba(252, 129, 129, 0.1);
|
||
color: #fed7d7;
|
||
}
|
||
.form-input.is-valid ~ .valid-check {
|
||
display: flex;
|
||
}
|
||
.form-input.is-invalid ~ .invalid-check {
|
||
display: flex;
|
||
}
|
||
|
||
/* Budget option - checked state via JS since has-[:checked] may not work in all contexts */
|
||
.budget-option.selected {
|
||
border-color: #0891b2;
|
||
background-color: rgb(8 145 178 / 0.1);
|
||
color: #0e7490;
|
||
}
|
||
|
||
/* FAQ item */
|
||
.faq-item summary::-webkit-details-marker { display: none; }
|
||
.faq-item summary::marker { display: none; }
|
||
|
||
/* Contact card top bar */
|
||
.contact-card .top-bar {
|
||
position: absolute;
|
||
top: 0; left: 0; right: 0;
|
||
height: 3px;
|
||
border-radius: 1rem 1rem 0 0;
|
||
}
|
||
</style>
|
||
|
||
<script>
|
||
import { trackFormSubmit, trackEvent } from '../utils/analytics';
|
||
|
||
// ── Scroll reveal ──────────────────────────────────────────
|
||
(function() {
|
||
const elements = document.querySelectorAll('.reveal-on-scroll');
|
||
if (!elements.length) return;
|
||
const observer = new IntersectionObserver(
|
||
(entries) => {
|
||
entries.forEach((entry) => {
|
||
if (entry.isIntersecting) {
|
||
entry.target.classList.add('revealed');
|
||
observer.unobserve(entry.target);
|
||
}
|
||
});
|
||
},
|
||
{ threshold: 0.12, rootMargin: '0px 0px -40px 0px' }
|
||
);
|
||
elements.forEach((el) => observer.observe(el));
|
||
})();
|
||
|
||
// ── Toast notifications ────────────────────────────────────
|
||
function showToast(message: string, type: 'success' | 'error') {
|
||
const container = document.getElementById('toast-container');
|
||
if (!container) return;
|
||
|
||
const toast = document.createElement('div');
|
||
toast.setAttribute('role', 'alert');
|
||
toast.setAttribute('aria-live', type === 'error' ? 'assertive' : 'polite');
|
||
toast.className = [
|
||
'pointer-events-auto px-5 py-4 rounded-xl shadow-xl transform transition-all duration-300',
|
||
'translate-x-full opacity-0 flex items-start gap-3 border',
|
||
type === 'success'
|
||
? 'bg-white border-emerald-200 text-secondary-900'
|
||
: 'bg-white border-red-200 text-secondary-900',
|
||
].join(' ');
|
||
|
||
const iconSvg = type === 'success'
|
||
? `<div class="flex-shrink-0 w-8 h-8 bg-emerald-100 rounded-full flex items-center justify-center">
|
||
<svg class="w-4 h-4 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7"/></svg>
|
||
</div>`
|
||
: `<div class="flex-shrink-0 w-8 h-8 bg-red-100 rounded-full flex items-center justify-center">
|
||
<svg class="w-4 h-4 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
||
</div>`;
|
||
|
||
const iconWrapper = document.createElement('div');
|
||
iconWrapper.innerHTML = iconSvg; // static, trusted SVG
|
||
const textEl = document.createElement('div');
|
||
textEl.className = 'flex-1 pt-1';
|
||
const msgEl = document.createElement('p');
|
||
msgEl.className = 'text-sm font-medium';
|
||
msgEl.textContent = message; // textContent prevents XSS
|
||
textEl.appendChild(msgEl);
|
||
toast.appendChild(iconWrapper);
|
||
toast.appendChild(textEl);
|
||
|
||
// Dismiss button
|
||
const dismissBtn = document.createElement('button');
|
||
dismissBtn.className = 'flex-shrink-0 mt-0.5 text-secondary-400 hover:text-secondary-600 transition-colors';
|
||
dismissBtn.setAttribute('aria-label', 'Dismiss notification');
|
||
dismissBtn.innerHTML = `<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>`;
|
||
dismissBtn.addEventListener('click', () => removeToast(toast));
|
||
toast.appendChild(dismissBtn);
|
||
|
||
container.appendChild(toast);
|
||
|
||
requestAnimationFrame(() => {
|
||
toast.classList.remove('translate-x-full', 'opacity-0');
|
||
});
|
||
|
||
const timer = setTimeout(() => removeToast(toast), 6000);
|
||
dismissBtn.addEventListener('click', () => clearTimeout(timer));
|
||
}
|
||
|
||
function removeToast(toast: HTMLElement) {
|
||
toast.classList.add('translate-x-full', 'opacity-0');
|
||
setTimeout(() => toast.remove(), 300);
|
||
}
|
||
|
||
// ── Character counter ──────────────────────────────────────
|
||
const messageField = document.getElementById('message') as HTMLTextAreaElement;
|
||
const charCount = document.getElementById('char-count');
|
||
if (messageField && charCount) {
|
||
messageField.addEventListener('input', () => {
|
||
const len = messageField.value.length;
|
||
charCount.textContent = `${len} / 5000`;
|
||
charCount.className = len > 4500 ? 'text-xs text-red-500' : 'text-xs text-secondary-400';
|
||
});
|
||
}
|
||
|
||
// ── Budget toggle (radio-as-chip) ──────────────────────────
|
||
const budgetOptions = document.querySelectorAll('.budget-option');
|
||
budgetOptions.forEach((opt) => {
|
||
const radio = opt.querySelector('input[type="radio"]') as HTMLInputElement;
|
||
if (radio) {
|
||
radio.addEventListener('change', () => {
|
||
budgetOptions.forEach((o) => o.classList.remove('selected'));
|
||
if (radio.checked) opt.classList.add('selected');
|
||
});
|
||
}
|
||
});
|
||
|
||
// ── Form Validation ────────────────────────────────────────
|
||
const form = document.getElementById('contact-form') as HTMLFormElement;
|
||
const successState = document.getElementById('success-state');
|
||
const sendAnotherBtn = document.getElementById('send-another');
|
||
|
||
if (form) {
|
||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||
const phoneRegex = /^[\+]?[(]?[0-9]{1,3}[)]?[-\s\.]?[(]?[0-9]{1,4}[)]?[-\s\.]?[0-9]{1,4}[-\s\.]?[0-9]{1,9}$/;
|
||
|
||
function setFieldState(fieldName: string, isValid: boolean | null) {
|
||
const errorEl = document.querySelector(`[data-error="${fieldName}"]`);
|
||
const inputEl = document.getElementById(fieldName) as HTMLElement | null;
|
||
const validCheck = inputEl?.parentElement?.querySelector('.valid-check') as HTMLElement | null;
|
||
const invalidCheck = inputEl?.parentElement?.querySelector('.invalid-check') as HTMLElement | null;
|
||
|
||
if (errorEl) errorEl.classList.toggle('hidden', isValid !== false);
|
||
|
||
if (inputEl) {
|
||
inputEl.classList.toggle('is-valid', isValid === true);
|
||
inputEl.classList.toggle('is-invalid', isValid === false);
|
||
}
|
||
if (validCheck) {
|
||
validCheck.classList.toggle('hidden', isValid !== true);
|
||
}
|
||
if (invalidCheck) {
|
||
invalidCheck.classList.toggle('hidden', isValid !== false);
|
||
}
|
||
}
|
||
|
||
function validateField(field: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement): boolean {
|
||
const name = field.name;
|
||
const value = field.value.trim();
|
||
|
||
switch (name) {
|
||
case 'name': {
|
||
const valid = value.length >= 2;
|
||
setFieldState('name', value.length === 0 ? null : valid);
|
||
return valid;
|
||
}
|
||
case 'email': {
|
||
const valid = emailRegex.test(value);
|
||
setFieldState('email', value.length === 0 ? null : valid);
|
||
return valid;
|
||
}
|
||
case 'phone':
|
||
if (value === '') { setFieldState('phone', null); return true; }
|
||
const phoneValid = phoneRegex.test(value);
|
||
setFieldState('phone', phoneValid);
|
||
return phoneValid;
|
||
case 'subject': {
|
||
const valid = value !== '';
|
||
setFieldState('subject', value === '' ? null : valid);
|
||
return valid;
|
||
}
|
||
case 'message': {
|
||
const valid = value.length >= 10;
|
||
setFieldState('message', value.length === 0 ? null : valid);
|
||
return valid;
|
||
}
|
||
default:
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// Blur + input event listeners
|
||
const inputs = form.querySelectorAll('input:not([type="radio"]), textarea, select');
|
||
inputs.forEach((input) => {
|
||
input.addEventListener('blur', (e) => {
|
||
validateField(e.target as HTMLInputElement);
|
||
});
|
||
input.addEventListener('input', (e) => {
|
||
const target = e.target as HTMLInputElement;
|
||
// Only re-validate if already touched (has a state class)
|
||
if (target.classList.contains('is-valid') || target.classList.contains('is-invalid')) {
|
||
validateField(target);
|
||
}
|
||
});
|
||
});
|
||
|
||
// Form submission
|
||
form.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
|
||
// Check honeypot
|
||
const honeypot = (form.elements.namedItem('website') as HTMLInputElement)?.value;
|
||
if (honeypot) {
|
||
showToast("Message sent successfully! We'll get back to you within 24 hours.", 'success');
|
||
form.reset();
|
||
return;
|
||
}
|
||
|
||
// Validate all fields
|
||
let isValid = true;
|
||
['name', 'email', 'phone', 'subject', 'message'].forEach((fieldName) => {
|
||
const field = form.elements.namedItem(fieldName) as HTMLInputElement;
|
||
if (field && !validateField(field)) isValid = false;
|
||
});
|
||
|
||
if (!isValid) {
|
||
showToast('Please fix the highlighted fields before submitting.', 'error');
|
||
// Focus first invalid field
|
||
const firstInvalid = form.querySelector('.is-invalid') as HTMLElement | null;
|
||
firstInvalid?.focus();
|
||
return;
|
||
}
|
||
|
||
// Loading state
|
||
const submitBtn = document.getElementById('submit-btn') as HTMLButtonElement;
|
||
const btnText = document.getElementById('btn-text');
|
||
const btnArrow = document.getElementById('btn-arrow');
|
||
const btnSpinner = document.getElementById('btn-spinner');
|
||
|
||
submitBtn.disabled = true;
|
||
if (btnText) btnText.textContent = 'Sending...';
|
||
if (btnArrow) btnArrow.classList.add('hidden');
|
||
if (btnSpinner) btnSpinner.classList.remove('hidden');
|
||
|
||
let success = false;
|
||
let errorMessage = 'Something went wrong. Please try again.';
|
||
|
||
try {
|
||
const response = await fetch('/api/contact', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
name: (form.elements.namedItem('name') as HTMLInputElement)?.value.trim(),
|
||
email: (form.elements.namedItem('email') as HTMLInputElement)?.value.trim(),
|
||
phone: (form.elements.namedItem('phone') as HTMLInputElement)?.value.trim(),
|
||
subject: (form.elements.namedItem('subject') as HTMLSelectElement)?.value,
|
||
message: (form.elements.namedItem('message') as HTMLTextAreaElement)?.value.trim(),
|
||
website: honeypot,
|
||
}),
|
||
});
|
||
|
||
const data = await response.json() as { success: boolean; message?: string; error?: string };
|
||
|
||
if (response.ok && data.success) {
|
||
success = true;
|
||
} else {
|
||
errorMessage = data.error ?? 'Failed to send message. Please try again.';
|
||
}
|
||
} catch {
|
||
errorMessage = 'Network error. Please check your connection and try again.';
|
||
}
|
||
|
||
// Track analytics
|
||
const subject = (form.elements.namedItem('subject') as HTMLSelectElement)?.value;
|
||
trackFormSubmit('contact', success);
|
||
if (success) trackEvent('contact_form_submit', { subject_category: subject });
|
||
|
||
// Reset button state
|
||
submitBtn.disabled = false;
|
||
if (btnText) btnText.textContent = 'Send Message';
|
||
if (btnArrow) btnArrow.classList.remove('hidden');
|
||
if (btnSpinner) btnSpinner.classList.add('hidden');
|
||
|
||
if (success) {
|
||
// Show success state inline
|
||
form.classList.add('hidden');
|
||
if (successState) successState.classList.remove('hidden');
|
||
showToast("Message sent! We'll get back to you within 24 hours.", 'success');
|
||
} else {
|
||
showToast(errorMessage, 'error');
|
||
}
|
||
});
|
||
|
||
// "Send another message" resets view
|
||
if (sendAnotherBtn) {
|
||
sendAnotherBtn.addEventListener('click', () => {
|
||
form.reset();
|
||
form.classList.remove('hidden');
|
||
if (successState) successState.classList.add('hidden');
|
||
// Reset field states
|
||
inputs.forEach((input) => {
|
||
(input as HTMLElement).classList.remove('is-valid', 'is-invalid');
|
||
});
|
||
form.querySelectorAll('.valid-check').forEach((el) => el.classList.add('hidden'));
|
||
form.querySelectorAll('.invalid-check').forEach((el) => el.classList.add('hidden'));
|
||
form.querySelectorAll('[data-error]').forEach((el) => el.classList.add('hidden'));
|
||
if (charCount) charCount.textContent = '0 / 5000';
|
||
budgetOptions.forEach((o) => o.classList.remove('selected'));
|
||
});
|
||
}
|
||
}
|
||
</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>
|