agent: Fix mobile responsiveness and conversion across pa
This commit is contained in:
+34
-17
@@ -85,23 +85,40 @@ const currentPath = Astro.url.pathname;
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu Button -->
|
||||
<button
|
||||
type="button"
|
||||
class="lg:hidden relative w-10 h-10 flex items-center justify-center text-secondary-600 dark:text-secondary-400 hover:text-primary dark:hover:text-primary-400 transition-colors"
|
||||
id="mobile-menu-toggle"
|
||||
aria-label="Toggle mobile menu"
|
||||
aria-expanded="false"
|
||||
aria-controls="mobile-menu"
|
||||
>
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<!-- Hamburger Icon -->
|
||||
<div class="hamburger-icon" aria-hidden="true">
|
||||
<span class="hamburger-line hamburger-line-1"></span>
|
||||
<span class="hamburger-line hamburger-line-2"></span>
|
||||
<span class="hamburger-line hamburger-line-3"></span>
|
||||
</div>
|
||||
</button>
|
||||
<!-- Mobile actions: prominent click-to-call + hamburger (shown <lg on every page) -->
|
||||
<div class="flex items-center gap-1 lg:hidden">
|
||||
<!-- Click-to-call — always visible in the mobile header so visitors on
|
||||
any page can reach us in one tap without opening the menu. -->
|
||||
<a
|
||||
href="tel:+919561417403"
|
||||
data-cta="header-mobile-call"
|
||||
aria-label="Call WorkRoot at +91 95614 17403"
|
||||
class="inline-flex items-center gap-1.5 h-10 px-3 rounded-lg bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 font-semibold text-sm hover:bg-primary-100 dark:hover:bg-primary-900/50 active:scale-95 transition-all focus:outline-none focus:ring-2 focus:ring-primary-400"
|
||||
>
|
||||
<svg class="w-5 h-5 flex-shrink-0" 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>
|
||||
<span class="hidden xs:inline">Call</span>
|
||||
</a>
|
||||
|
||||
<!-- Mobile Menu Button -->
|
||||
<button
|
||||
type="button"
|
||||
class="relative w-10 h-10 flex items-center justify-center text-secondary-600 dark:text-secondary-400 hover:text-primary dark:hover:text-primary-400 transition-colors"
|
||||
id="mobile-menu-toggle"
|
||||
aria-label="Toggle mobile menu"
|
||||
aria-expanded="false"
|
||||
aria-controls="mobile-menu"
|
||||
>
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<!-- Hamburger Icon -->
|
||||
<div class="hamburger-icon" aria-hidden="true">
|
||||
<span class="hamburger-line hamburger-line-1"></span>
|
||||
<span class="hamburger-line hamburger-line-2"></span>
|
||||
<span class="hamburger-line hamburger-line-3"></span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
|
||||
+132
-98
@@ -80,6 +80,23 @@ const trustStats = [
|
||||
{ value: '< 24h', label: 'Response Time' },
|
||||
{ value: '3+', label: 'Years Experience' },
|
||||
];
|
||||
|
||||
// Service options — rendered as large, tappable radio tiles (replacing the
|
||||
// old native <select>) so the choice is thumb-friendly on mobile. `value`
|
||||
// is the code submitted to /api/contact (unchanged from the previous
|
||||
// <option> values); `label` is the display text.
|
||||
const serviceOptions = [
|
||||
{ value: 'erp-solutions', label: 'ERP / Enterprise Systems' },
|
||||
{ value: 'government-systems', label: 'Government IT' },
|
||||
{ value: 'web-development', label: 'Web Development' },
|
||||
{ value: 'mobile-development', label: 'Mobile Development' },
|
||||
{ value: 'ui-ux', label: 'UI/UX Design' },
|
||||
{ value: 'consulting', label: 'IT Consulting / DevOps' },
|
||||
{ value: 'other', label: 'Other / General Inquiry' },
|
||||
];
|
||||
|
||||
// Budget options — also rendered as large tappable tiles.
|
||||
const budgetOptions = ['< ₹1L', '₹1L – ₹5L', '₹5L – ₹15L', '₹15L+', 'Not sure yet'];
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
@@ -273,107 +290,84 @@ const trustStats = [
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Phone + Subject row -->
|
||||
<div class="grid sm:grid-cols-2 gap-5">
|
||||
<!-- Phone Field -->
|
||||
<div class="form-group">
|
||||
<label for="phone" class="block text-sm font-semibold text-secondary-700 dark:text-secondary-300 mb-1.5">
|
||||
Phone Number
|
||||
<span class="text-secondary-400 font-normal">(optional)</span>
|
||||
</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none" aria-hidden="true">
|
||||
<svg class="w-4 h-4 text-secondary-400 field-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
type="tel"
|
||||
id="phone"
|
||||
name="phone"
|
||||
autocomplete="tel"
|
||||
aria-describedby="phone-error"
|
||||
class="form-input w-full pl-10 pr-4 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 dark:text-secondary-100 placeholder-secondary-400 dark:placeholder-secondary-500 text-sm bg-secondary-50 dark:bg-secondary-700 focus:bg-white dark:focus:bg-secondary-700"
|
||||
placeholder="+1 (555) 123-4567"
|
||||
/>
|
||||
<div class="valid-check absolute inset-y-0 right-3 flex items-center hidden" aria-label="Valid">
|
||||
<svg class="w-4 h-4 text-green-700 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="invalid-check absolute inset-y-0 right-3 flex items-center hidden" aria-label="Invalid">
|
||||
<svg class="w-4 h-4 text-red-800 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</div>
|
||||
<!-- Phone Field (full width) -->
|
||||
<div class="form-group">
|
||||
<label for="phone" class="block text-sm font-semibold text-secondary-700 dark:text-secondary-300 mb-1.5">
|
||||
Phone Number
|
||||
<span class="text-secondary-400 font-normal">(optional)</span>
|
||||
</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none" aria-hidden="true">
|
||||
<svg class="w-4 h-4 text-secondary-400 field-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="mt-1.5 text-xs text-red-500 hidden flex items-center gap-1" id="phone-error" data-error="phone" role="alert">
|
||||
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
|
||||
Please enter a valid phone number.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Subject Field -->
|
||||
<div class="form-group">
|
||||
<label for="subject" class="block text-sm font-semibold text-secondary-700 dark:text-secondary-300 mb-1.5">
|
||||
Service Needed <span class="text-red-500" aria-hidden="true">*</span>
|
||||
<span class="sr-only">(required)</span>
|
||||
</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none" aria-hidden="true">
|
||||
<svg class="w-4 h-4 text-secondary-400 field-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||||
</svg>
|
||||
</div>
|
||||
<select
|
||||
id="subject"
|
||||
name="subject"
|
||||
required
|
||||
aria-required="true"
|
||||
aria-describedby="subject-error"
|
||||
class="form-input w-full pl-10 pr-8 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 dark:text-secondary-100 text-sm bg-secondary-50 dark:bg-secondary-700 focus:bg-white dark:focus:bg-secondary-700 appearance-none cursor-pointer"
|
||||
>
|
||||
<option value="">Select a service...</option>
|
||||
<option value="erp-solutions">ERP / Enterprise Systems</option>
|
||||
<option value="government-systems">Government IT</option>
|
||||
<option value="web-development">Web Development</option>
|
||||
<option value="mobile-development">Mobile Development</option>
|
||||
<option value="ui-ux">UI/UX Design</option>
|
||||
<option value="consulting">IT Consulting / DevOps</option>
|
||||
<option value="other">Other / General Inquiry</option>
|
||||
</select>
|
||||
<div class="valid-check absolute inset-y-0 right-9 flex items-center hidden" aria-label="Valid">
|
||||
<svg class="w-4 h-4 text-green-700 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="invalid-check absolute inset-y-0 right-9 flex items-center hidden" aria-label="Invalid">
|
||||
<svg class="w-4 h-4 text-red-800 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="absolute inset-y-0 right-3 flex items-center pointer-events-none" aria-hidden="true">
|
||||
<svg class="w-4 h-4 text-secondary-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
type="tel"
|
||||
id="phone"
|
||||
name="phone"
|
||||
autocomplete="tel"
|
||||
aria-describedby="phone-error"
|
||||
class="form-input w-full pl-10 pr-4 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 dark:text-secondary-100 placeholder-secondary-400 dark:placeholder-secondary-500 text-sm bg-secondary-50 dark:bg-secondary-700 focus:bg-white dark:focus:bg-secondary-700"
|
||||
placeholder="+1 (555) 123-4567"
|
||||
/>
|
||||
<div class="valid-check absolute inset-y-0 right-3 flex items-center hidden" aria-label="Valid">
|
||||
<svg class="w-4 h-4 text-green-700 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="invalid-check absolute inset-y-0 right-3 flex items-center hidden" aria-label="Invalid">
|
||||
<svg class="w-4 h-4 text-red-800 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="mt-1.5 text-xs text-red-500 hidden flex items-center gap-1" id="subject-error" data-error="subject" role="alert">
|
||||
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
|
||||
Please select a service.
|
||||
</p>
|
||||
</div>
|
||||
<p class="mt-1.5 text-xs text-red-500 hidden flex items-center gap-1" id="phone-error" data-error="phone" role="alert">
|
||||
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
|
||||
Please enter a valid phone number.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Budget range (new field) -->
|
||||
<!-- Service Needed — large tappable radio tiles (replaces the
|
||||
old native <select>). Renders 2 columns on mobile (each tile
|
||||
a comfortable thumb target) and 3 from sm up. The hidden
|
||||
radios share name="subject" so the form submission and
|
||||
analytics read the same value the <select> used to provide. -->
|
||||
<div class="form-group">
|
||||
<span id="subject-label" class="block text-sm font-semibold text-secondary-700 dark:text-secondary-300 mb-2">
|
||||
Service Needed <span class="text-red-500" aria-hidden="true">*</span>
|
||||
<span class="sr-only">(required)</span>
|
||||
</span>
|
||||
<div
|
||||
id="subject-group"
|
||||
role="radiogroup"
|
||||
aria-labelledby="subject-label"
|
||||
aria-describedby="subject-error"
|
||||
class="grid grid-cols-2 sm:grid-cols-3 gap-2.5"
|
||||
>
|
||||
{serviceOptions.map((opt) => (
|
||||
<label class="subject-option group flex items-center justify-center text-center min-h-[52px] px-3 py-2.5 rounded-xl border border-secondary-200 dark:border-secondary-600 text-sm font-medium text-secondary-700 dark:text-secondary-300 cursor-pointer hover:border-primary hover:bg-primary/5 dark:hover:bg-primary/10 transition-all duration-150 has-[:checked]:border-primary has-[:checked]:bg-primary/10 has-[:checked]:text-primary-700 dark:has-[:checked]:text-primary-300 has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-primary/40">
|
||||
<input type="radio" name="subject" value={opt.value} class="sr-only" aria-describedby="subject-error" />
|
||||
<span class="leading-snug">{opt.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<p class="mt-1.5 text-xs text-red-500 hidden flex items-center gap-1" id="subject-error" data-error="subject" role="alert">
|
||||
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
|
||||
Please select a service.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Project Budget — large tappable radio tiles. 2 cols on mobile
|
||||
so each is a full thumb target; auto-fit row from sm up. -->
|
||||
<div class="form-group">
|
||||
<label class="block text-sm font-semibold text-secondary-700 dark:text-secondary-300 mb-2">
|
||||
Project Budget
|
||||
<span class="text-secondary-400 dark:text-secondary-500 font-normal">(optional)</span>
|
||||
</label>
|
||||
<div class="flex flex-wrap gap-2" role="group" aria-label="Budget range options">
|
||||
{['< ₹1L', '₹1L – ₹5L', '₹5L – ₹15L', '₹15L+', 'Not sure yet'].map((range, i) => (
|
||||
<label class="budget-option flex items-center gap-1.5 px-3 py-2 rounded-lg border border-secondary-200 dark:border-secondary-600 text-sm text-secondary-600 dark:text-secondary-400 cursor-pointer hover:border-primary hover:bg-primary/5 dark:hover:bg-primary/10 transition-all duration-150 has-[:checked]:border-primary has-[:checked]:bg-primary/10 has-[:checked]:text-primary-700 dark:has-[:checked]:text-primary-300">
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-2.5" role="group" aria-label="Budget range options">
|
||||
{budgetOptions.map((range) => (
|
||||
<label class="budget-option flex items-center justify-center text-center min-h-[48px] px-3 py-2.5 rounded-xl border border-secondary-200 dark:border-secondary-600 text-sm font-medium text-secondary-600 dark:text-secondary-400 cursor-pointer hover:border-primary hover:bg-primary/5 dark:hover:bg-primary/10 transition-all duration-150 has-[:checked]:border-primary has-[:checked]:bg-primary/10 has-[:checked]:text-primary-700 dark:has-[:checked]:text-primary-300 has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-primary/40">
|
||||
<input type="radio" name="budget" value={range} class="sr-only" />
|
||||
{range}
|
||||
</label>
|
||||
@@ -806,6 +800,13 @@ const trustStats = [
|
||||
color: #0e7490;
|
||||
}
|
||||
|
||||
/* Service tiles: invalid-group highlight (shown only after a failed submit
|
||||
attempt with no service chosen). Subtle red ring around the whole group. */
|
||||
.subject-group-invalid {
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 0 0 2px rgb(220 38 38 / 0.35);
|
||||
}
|
||||
|
||||
/* FAQ item */
|
||||
.faq-item summary::-webkit-details-marker { display: none; }
|
||||
.faq-item summary::marker { display: none; }
|
||||
@@ -909,7 +910,7 @@ const trustStats = [
|
||||
});
|
||||
}
|
||||
|
||||
// ── Budget toggle (radio-as-chip) ──────────────────────────
|
||||
// ── Budget toggle (radio-as-tile) ──────────────────────────
|
||||
const budgetOptions = document.querySelectorAll('.budget-option');
|
||||
budgetOptions.forEach((opt) => {
|
||||
const radio = opt.querySelector('input[type="radio"]') as HTMLInputElement;
|
||||
@@ -921,6 +922,31 @@ const trustStats = [
|
||||
}
|
||||
});
|
||||
|
||||
// ── Service tiles (radio group replacing the old native <select>) ──
|
||||
// The hidden radios share name="subject" so `form.elements.subject` resolves
|
||||
// to a RadioNodeList whose `.value` is the checked option — exactly what the
|
||||
// submit handler and analytics previously read from the <select>.
|
||||
const subjectErrorEl = document.querySelector('[data-error="subject"]');
|
||||
const subjectGroupEl = document.getElementById('subject-group');
|
||||
const subjectRadios = Array.from(
|
||||
document.querySelectorAll<HTMLInputElement>('input[name="subject"]')
|
||||
);
|
||||
|
||||
function getSubjectValue(): string {
|
||||
return subjectRadios.find((r) => r.checked)?.value ?? '';
|
||||
}
|
||||
|
||||
function validateSubject(touched = true): boolean {
|
||||
const valid = getSubjectValue() !== '';
|
||||
if (subjectErrorEl) subjectErrorEl.classList.toggle('hidden', valid || !touched);
|
||||
subjectGroupEl?.classList.toggle('subject-group-invalid', !valid && touched);
|
||||
return valid;
|
||||
}
|
||||
|
||||
subjectRadios.forEach((radio) => {
|
||||
radio.addEventListener('change', () => validateSubject());
|
||||
});
|
||||
|
||||
// ── Form Validation ────────────────────────────────────────
|
||||
const form = document.getElementById('contact-form') as HTMLFormElement;
|
||||
const successState = document.getElementById('success-state');
|
||||
@@ -1012,18 +1038,25 @@ const trustStats = [
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate all fields
|
||||
// Validate all fields. `subject` is now a radio group, so it is checked
|
||||
// separately from the text/textarea fields handled by validateField.
|
||||
let isValid = true;
|
||||
['name', 'email', 'phone', 'subject', 'message'].forEach((fieldName) => {
|
||||
['name', 'email', 'phone', 'message'].forEach((fieldName) => {
|
||||
const field = form.elements.namedItem(fieldName) as HTMLInputElement;
|
||||
if (field && !validateField(field)) isValid = false;
|
||||
});
|
||||
if (!validateSubject()) isValid = false;
|
||||
|
||||
if (!isValid) {
|
||||
showToast('Please fix the highlighted fields before submitting.', 'error');
|
||||
// Focus first invalid field
|
||||
// Focus the first invalid text field, or the service tiles if that's
|
||||
// the only thing missing.
|
||||
const firstInvalid = form.querySelector('.is-invalid') as HTMLElement | null;
|
||||
firstInvalid?.focus();
|
||||
if (firstInvalid) {
|
||||
firstInvalid.focus();
|
||||
} else if (getSubjectValue() === '') {
|
||||
subjectRadios[0]?.focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1057,7 +1090,7 @@ const trustStats = [
|
||||
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,
|
||||
subject: getSubjectValue(),
|
||||
message: (form.elements.namedItem('message') as HTMLTextAreaElement)?.value.trim(),
|
||||
website: honeypot,
|
||||
}),
|
||||
@@ -1087,7 +1120,7 @@ const trustStats = [
|
||||
}
|
||||
|
||||
// Track analytics
|
||||
const subject = (form.elements.namedItem('subject') as HTMLSelectElement)?.value;
|
||||
const subject = getSubjectValue();
|
||||
trackFormSubmit('contact', success);
|
||||
if (success) trackEvent('contact_form_submit', { subject_category: subject });
|
||||
|
||||
@@ -1122,6 +1155,7 @@ const trustStats = [
|
||||
form.querySelectorAll('[data-error]').forEach((el) => el.classList.add('hidden'));
|
||||
if (charCount) charCount.textContent = '0 / 2000';
|
||||
budgetOptions.forEach((o) => o.classList.remove('selected'));
|
||||
subjectGroupEl?.classList.remove('subject-group-invalid');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -400,6 +400,52 @@ const techStack = [
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Mid-page Conversion CTA — sits directly after the Services section so
|
||||
mobile visitors hit the funnel early instead of having to scroll past
|
||||
How-It-Works, testimonials, the tech stack and the FAQ to reach the
|
||||
inline lead form. Primary action jumps straight to the on-page inline
|
||||
lead form (#inline-lead-form, smooth-scrolled by the anchor handler at
|
||||
the bottom of this page); secondary action is a one-tap phone call. -->
|
||||
<section
|
||||
aria-labelledby="midpage-cta-heading"
|
||||
class="bg-gradient-to-r from-primary-600 to-primary-700 dark:from-primary-700 dark:to-primary-800"
|
||||
>
|
||||
<div class="container-wrapper py-10 sm:py-12">
|
||||
<div class="flex flex-col items-center gap-5 text-center md:flex-row md:justify-between md:text-left md:gap-8 max-w-5xl mx-auto">
|
||||
<div class="md:flex-1">
|
||||
<h2 id="midpage-cta-heading" class="text-2xl sm:text-3xl font-bold text-white leading-tight">
|
||||
Have a project in mind? Get a free proposal in 24 hours.
|
||||
</h2>
|
||||
<p class="mt-2 text-primary-100 text-sm sm:text-base">
|
||||
No obligation · Free 30-min discovery call · NDA on request.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex w-full flex-col sm:flex-row md:w-auto md:flex-shrink-0 gap-3">
|
||||
<a
|
||||
href="#inline-lead-form"
|
||||
data-cta="midpage-primary"
|
||||
class="inline-flex items-center justify-center rounded-xl bg-white px-7 py-4 text-base font-bold text-primary-700 shadow-lg transition-all hover:bg-primary-50 hover:scale-105 focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-primary-700"
|
||||
>
|
||||
Get my free proposal
|
||||
<svg class="ml-2 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.5" d="M17 8l4 4m0 0l-4 4m4-4H3"></path>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="tel:+919561417403"
|
||||
data-cta="midpage-call"
|
||||
class="inline-flex items-center justify-center rounded-xl border-2 border-white/40 bg-white/5 px-7 py-4 text-base font-bold text-white transition-all hover:bg-white/15 hover:border-white/60 focus:outline-none focus:ring-2 focus:ring-white/60 focus:ring-offset-2 focus:ring-offset-primary-700"
|
||||
>
|
||||
<svg class="mr-2 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 now
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- How It Works Section - NEW -->
|
||||
<section class="py-24 bg-gradient-to-br from-secondary-900 via-secondary-800 to-primary-900 relative overflow-hidden">
|
||||
<!-- Background pattern -->
|
||||
|
||||
@@ -348,8 +348,11 @@ const featuredProjects = projects.filter(p => p.featured);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category Filter Tabs -->
|
||||
<div class="flex flex-wrap justify-center gap-3" role="tablist" aria-label="Filter projects by category">
|
||||
<!-- Category Filter Tabs
|
||||
Mobile (<sm): a single horizontally-scrollable row so the five
|
||||
chips never wrap into a tall stack that pushes the project grid
|
||||
below the fold. ≥sm: centered wrapping row as before. -->
|
||||
<div class="filter-scroll flex flex-nowrap gap-3 overflow-x-auto pb-1 snap-x sm:flex-wrap sm:justify-center sm:overflow-visible sm:pb-0" role="tablist" aria-label="Filter projects by category">
|
||||
<button
|
||||
data-filter="all"
|
||||
class="filter-btn active rounded-full bg-primary px-6 py-2.5 font-semibold text-white transition-all duration-300 hover:shadow-lg hover:shadow-primary/25 focus:outline-none focus:ring-2 focus:ring-primary-400 focus:ring-offset-2"
|
||||
@@ -1190,4 +1193,28 @@ const featuredProjects = projects.filter(p => p.featured);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Horizontally-scrollable category filter row on mobile.
|
||||
Chips keep their intrinsic width (no shrinking) and snap as the user
|
||||
swipes; the scrollbar is hidden for a clean native-app feel. At >=sm the
|
||||
row wraps normally so the scroll affordance is mobile-only. */
|
||||
.filter-scroll {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scroll-snap-type: x proximity;
|
||||
}
|
||||
.filter-scroll > .filter-btn {
|
||||
flex: 0 0 auto;
|
||||
white-space: nowrap;
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
/* Hide the scrollbar (still scrollable) only while in the mobile scroll mode */
|
||||
@media (max-width: 639px) {
|
||||
.filter-scroll {
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE/Edge legacy */
|
||||
}
|
||||
.filter-scroll::-webkit-scrollbar {
|
||||
display: none; /* Chrome/Safari */
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user