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
1029 lines
55 KiB
Plaintext
1029 lines
55 KiB
Plaintext
---
|
||
// 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>
|