Files
CompanySite/src/pages/contact.astro
T
Clintchiz d402256547
Deploy to Production / Build & Verify (push) Failing after 5m56s
Ping Search Engines / Notify Search Engines (push) Successful in 2s
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 11m26s
E2E Test Suite / Form Interaction Tests (push) Failing after 11m42s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 12m2s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 16m14s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 17m45s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 25m23s
E2E Test Suite / Security Header Tests (push) Failing after 7m55s
E2E Test Suite / Test Report Summary (push) Failing after 20s
E2E Test Suite / Mobile Device Tests (push) Failing after 2h49m9s
Uptime Monitor / Health & Response Time (push) Failing after 2s
Uptime Monitor / SSL Certificate (push) Successful in 2s
Uptime Monitor / Send Alerts (push) Failing after 3s
Uptime Monitor / Record Uptime Success (push) Has been skipped
First Init
2026-03-21 16:46:46 +05:30

1029 lines
55 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
// Contact Page for WorkRoot IT Solutions
// 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: ['123 Tech Plaza, Suite 400', 'San Francisco, CA 94105'],
link: 'https://maps.google.com/?q=123+Tech+Plaza+San+Francisco+CA',
linkLabel: 'Get directions',
color: 'primary',
},
{
icon: 'phone',
title: 'Call Us',
details: ['+1 (555) 123-4567', '+1 (555) 987-6543'],
link: 'tel:+15551234567',
linkLabel: 'Call now',
color: 'accent',
},
{
icon: 'email',
title: 'Email Us',
details: ['hello@workroot.in', 'support@workroot.in'],
link: 'mailto:hello@workroot.in',
linkLabel: 'Send email',
color: 'emerald',
},
{
icon: 'clock',
title: 'Business Hours',
details: ['Mon Fri: 9:00 AM 6:00 PM', 'Sat Sun: Closed'],
link: null,
linkLabel: null,
color: 'violet',
},
];
// Social links
const socialLinks = [
{
name: 'LinkedIn',
href: '#',
icon: 'linkedin',
description: 'Connect professionally',
color: 'hover:bg-[#0077b5]',
},
{
name: 'Twitter / X',
href: '#',
icon: 'twitter',
description: 'Follow for updates',
color: 'hover:bg-[#000000]',
},
{
name: 'GitHub',
href: '#',
icon: 'github',
description: 'See our open source',
color: 'hover:bg-[#333333]',
},
{
name: 'Instagram',
href: '#',
icon: 'instagram',
description: 'Behind the scenes',
color: 'hover:bg-gradient-to-br hover:from-[#f9ce34] hover:via-[#ee2a7b] hover:to-[#6228d7]',
},
];
// Trust indicators
const trustStats = [
{ value: '500+', label: 'Projects Delivered' },
{ value: '98%', label: 'Client Satisfaction' },
{ value: '< 24h', label: 'Response Time' },
{ value: '10+', label: 'Years Experience' },
];
---
<BaseLayout
title="Contact Us — Get a Free Quote"
description="Start your project with WorkRoot IT Solutions. 500+ projects delivered, 98% client satisfaction, response within 24 hours. Reach us at hello@workroot.in or call +1 (555) 123-4567."
>
<SEO
slot="head"
type="ContactPage"
breadcrumbs={[
{ name: 'Home', url: '/' },
{ name: 'Contact', url: '/contact' }
]}
/>
<!-- Prefetch Google Maps domain used for the directions link on this page -->
<link slot="head" rel="dns-prefetch" href="https://maps.google.com" />
<link slot="head" rel="preconnect" href="https://maps.google.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-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-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-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">
<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 rounded-2xl shadow-card border border-secondary-100 p-8 lg:p-10">
<!-- Form Header -->
<div class="mb-8">
<h2 class="text-2xl font-bold text-secondary-900 mb-2">Send Us a Message</h2>
<p class="text-secondary-500 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 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 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 placeholder-secondary-400 text-sm bg-secondary-50 focus:bg-white"
placeholder="John Doe"
/>
<div class="valid-check absolute inset-y-0 right-3 flex items-center hidden" aria-hidden="true">
<svg class="w-4 h-4 text-emerald-500" 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>
<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"><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 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 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 placeholder-secondary-400 text-sm bg-secondary-50 focus:bg-white"
placeholder="john@example.com"
/>
<div class="valid-check absolute inset-y-0 right-3 flex items-center hidden" aria-hidden="true">
<svg class="w-4 h-4 text-emerald-500" 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>
<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"><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 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 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 placeholder-secondary-400 text-sm bg-secondary-50 focus:bg-white"
placeholder="+1 (555) 123-4567"
/>
</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"><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 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 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 text-sm bg-secondary-50 focus:bg-white 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="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"><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 mb-2">
Project Budget
<span class="text-secondary-400 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 text-sm text-secondary-600 cursor-pointer hover:border-primary hover:bg-primary/5 transition-all duration-150 has-[:checked]:border-primary has-[:checked]:bg-primary/10 has-[:checked]:text-primary-700">
<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">
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">0 / 5000</span>
</div>
<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 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 placeholder-secondary-400 text-sm bg-secondary-50 focus:bg-white resize-none"
placeholder="Tell us about your project, goals, and timeline..."
></textarea>
<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"><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 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 mb-2">Message Sent!</h3>
<p class="text-secondary-600 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 rounded-2xl p-5 shadow-card border border-secondary-100 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 mb-1.5">{info.title}</h3>
{info.details.map((detail) => (
<p class="text-xs text-secondary-500 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 rounded-2xl shadow-card border border-secondary-100 overflow-hidden">
<div class="relative h-52 bg-secondary-100 group">
<!-- Static map placeholder with illustrated style -->
<div class="absolute inset-0 bg-gradient-to-br from-secondary-100 via-secondary-50 to-primary-50 flex flex-col items-center justify-center">
<!-- Map pin SVG illustration -->
<div class="relative mb-3">
<div class="w-14 h-14 bg-primary rounded-full flex items-center justify-center shadow-primary-glow animate-float">
<svg class="w-7 h-7 text-white" 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>
</div>
<!-- Ping rings -->
<div class="absolute inset-0 rounded-full border-2 border-primary/30 animate-ping"></div>
</div>
<p class="text-sm font-semibold text-secondary-700">123 Tech Plaza</p>
<p class="text-xs text-secondary-500">San Francisco, CA 94105</p>
</div>
<!-- Hover overlay: Get Directions -->
<a
href="https://maps.google.com/?q=123+Tech+Plaza+San+Francisco+CA"
target="_blank"
rel="noopener noreferrer"
aria-label="Get directions to 123 Tech Plaza, San Francisco (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 -->
<div class="px-5 py-4 flex items-center justify-between border-t border-secondary-100">
<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 font-medium">Open now</span>
<span class="text-xs text-secondary-400">· Closes at 6 PM</span>
</div>
<a
href="https://maps.google.com/?q=123+Tech+Plaza+San+Francisco+CA"
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 === 'instagram' && (
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/>
</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 border-t border-secondary-100">
<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 mb-3">Common Questions</h2>
<p class="text-secondary-500 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 San Francisco?',
a: 'Absolutely. We work with clients 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 rounded-xl border border-secondary-100 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 transition-colors duration-150">
<span class="font-semibold text-secondary-900 text-sm">{faq.q}</span>
<div class="flex-shrink-0 w-6 h-6 rounded-full bg-secondary-200 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 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:+15551234567"
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 */
.form-input.is-valid {
border-color: #10b981;
background-color: #f0fdf4;
}
.form-input.is-invalid {
border-color: #ef4444;
background-color: #fef2f2;
}
.form-input.is-valid ~ .valid-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;
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);
}
}
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('[data-error]').forEach((el) => el.classList.add('hidden'));
if (charCount) charCount.textContent = '0 / 5000';
budgetOptions.forEach((o) => o.classList.remove('selected'));
});
}
}
</script>