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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile Menu Button -->
|
<!-- Mobile actions: prominent click-to-call + hamburger (shown <lg on every page) -->
|
||||||
<button
|
<div class="flex items-center gap-1 lg:hidden">
|
||||||
type="button"
|
<!-- Click-to-call — always visible in the mobile header so visitors on
|
||||||
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"
|
any page can reach us in one tap without opening the menu. -->
|
||||||
id="mobile-menu-toggle"
|
<a
|
||||||
aria-label="Toggle mobile menu"
|
href="tel:+919561417403"
|
||||||
aria-expanded="false"
|
data-cta="header-mobile-call"
|
||||||
aria-controls="mobile-menu"
|
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"
|
||||||
<span class="sr-only">Open main menu</span>
|
>
|
||||||
<!-- Hamburger Icon -->
|
<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
<div class="hamburger-icon" 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" />
|
||||||
<span class="hamburger-line hamburger-line-1"></span>
|
</svg>
|
||||||
<span class="hamburger-line hamburger-line-2"></span>
|
<span class="hidden xs:inline">Call</span>
|
||||||
<span class="hamburger-line hamburger-line-3"></span>
|
</a>
|
||||||
</div>
|
|
||||||
</button>
|
<!-- 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>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+132
-98
@@ -80,6 +80,23 @@ const trustStats = [
|
|||||||
{ value: '< 24h', label: 'Response Time' },
|
{ value: '< 24h', label: 'Response Time' },
|
||||||
{ value: '3+', label: 'Years Experience' },
|
{ 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
|
<BaseLayout
|
||||||
@@ -273,107 +290,84 @@ const trustStats = [
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Phone + Subject row -->
|
<!-- Phone Field (full width) -->
|
||||||
<div class="grid sm:grid-cols-2 gap-5">
|
<div class="form-group">
|
||||||
<!-- Phone Field -->
|
<label for="phone" class="block text-sm font-semibold text-secondary-700 dark:text-secondary-300 mb-1.5">
|
||||||
<div class="form-group">
|
Phone Number
|
||||||
<label for="phone" class="block text-sm font-semibold text-secondary-700 dark:text-secondary-300 mb-1.5">
|
<span class="text-secondary-400 font-normal">(optional)</span>
|
||||||
Phone Number
|
</label>
|
||||||
<span class="text-secondary-400 font-normal">(optional)</span>
|
<div class="relative">
|
||||||
</label>
|
<div class="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none" aria-hidden="true">
|
||||||
<div class="relative">
|
<svg class="w-4 h-4 text-secondary-400 field-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none" 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 class="w-4 h-4 text-secondary-400 field-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
</svg>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="tel"
|
|
||||||
id="phone"
|
|
||||||
name="phone"
|
|
||||||
autocomplete="tel"
|
|
||||||
aria-describedby="phone-error"
|
|
||||||
class="form-input w-full pl-10 pr-4 py-3 rounded-xl border border-secondary-200 dark:border-secondary-600 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-200 text-secondary-900 dark:text-secondary-100 placeholder-secondary-400 dark:placeholder-secondary-500 text-sm bg-secondary-50 dark:bg-secondary-700 focus:bg-white dark:focus:bg-secondary-700"
|
|
||||||
placeholder="+1 (555) 123-4567"
|
|
||||||
/>
|
|
||||||
<div class="valid-check absolute inset-y-0 right-3 flex items-center hidden" aria-label="Valid">
|
|
||||||
<svg class="w-4 h-4 text-green-700 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="invalid-check absolute inset-y-0 right-3 flex items-center hidden" aria-label="Invalid">
|
|
||||||
<svg class="w-4 h-4 text-red-800 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
</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">
|
<input
|
||||||
<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>
|
type="tel"
|
||||||
Please enter a valid phone number.
|
id="phone"
|
||||||
</p>
|
name="phone"
|
||||||
</div>
|
autocomplete="tel"
|
||||||
|
aria-describedby="phone-error"
|
||||||
<!-- Subject Field -->
|
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"
|
||||||
<div class="form-group">
|
placeholder="+1 (555) 123-4567"
|
||||||
<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>
|
<div class="valid-check absolute inset-y-0 right-3 flex items-center hidden" aria-label="Valid">
|
||||||
<span class="sr-only">(required)</span>
|
<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">
|
||||||
</label>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" />
|
||||||
<div class="relative">
|
</svg>
|
||||||
<div class="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none" aria-hidden="true">
|
</div>
|
||||||
<svg class="w-4 h-4 text-secondary-400 field-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<div class="invalid-check absolute inset-y-0 right-3 flex items-center hidden" aria-label="Invalid">
|
||||||
<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 class="w-4 h-4 text-red-800 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
</svg>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" />
|
||||||
</div>
|
</svg>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1.5 text-xs text-red-500 hidden flex items-center gap-1" id="subject-error" data-error="subject" role="alert">
|
|
||||||
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
|
|
||||||
Please select a service.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<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>
|
</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">
|
<div class="form-group">
|
||||||
<label class="block text-sm font-semibold text-secondary-700 dark:text-secondary-300 mb-2">
|
<label class="block text-sm font-semibold text-secondary-700 dark:text-secondary-300 mb-2">
|
||||||
Project Budget
|
Project Budget
|
||||||
<span class="text-secondary-400 dark:text-secondary-500 font-normal">(optional)</span>
|
<span class="text-secondary-400 dark:text-secondary-500 font-normal">(optional)</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="flex flex-wrap gap-2" role="group" aria-label="Budget range options">
|
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-2.5" role="group" aria-label="Budget range options">
|
||||||
{['< ₹1L', '₹1L – ₹5L', '₹5L – ₹15L', '₹15L+', 'Not sure yet'].map((range, i) => (
|
{budgetOptions.map((range) => (
|
||||||
<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">
|
<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" />
|
<input type="radio" name="budget" value={range} class="sr-only" />
|
||||||
{range}
|
{range}
|
||||||
</label>
|
</label>
|
||||||
@@ -806,6 +800,13 @@ const trustStats = [
|
|||||||
color: #0e7490;
|
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 */
|
||||||
.faq-item summary::-webkit-details-marker { display: none; }
|
.faq-item summary::-webkit-details-marker { display: none; }
|
||||||
.faq-item summary::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');
|
const budgetOptions = document.querySelectorAll('.budget-option');
|
||||||
budgetOptions.forEach((opt) => {
|
budgetOptions.forEach((opt) => {
|
||||||
const radio = opt.querySelector('input[type="radio"]') as HTMLInputElement;
|
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 ────────────────────────────────────────
|
// ── Form Validation ────────────────────────────────────────
|
||||||
const form = document.getElementById('contact-form') as HTMLFormElement;
|
const form = document.getElementById('contact-form') as HTMLFormElement;
|
||||||
const successState = document.getElementById('success-state');
|
const successState = document.getElementById('success-state');
|
||||||
@@ -1012,18 +1038,25 @@ const trustStats = [
|
|||||||
return;
|
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;
|
let isValid = true;
|
||||||
['name', 'email', 'phone', 'subject', 'message'].forEach((fieldName) => {
|
['name', 'email', 'phone', 'message'].forEach((fieldName) => {
|
||||||
const field = form.elements.namedItem(fieldName) as HTMLInputElement;
|
const field = form.elements.namedItem(fieldName) as HTMLInputElement;
|
||||||
if (field && !validateField(field)) isValid = false;
|
if (field && !validateField(field)) isValid = false;
|
||||||
});
|
});
|
||||||
|
if (!validateSubject()) isValid = false;
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
showToast('Please fix the highlighted fields before submitting.', 'error');
|
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;
|
const firstInvalid = form.querySelector('.is-invalid') as HTMLElement | null;
|
||||||
firstInvalid?.focus();
|
if (firstInvalid) {
|
||||||
|
firstInvalid.focus();
|
||||||
|
} else if (getSubjectValue() === '') {
|
||||||
|
subjectRadios[0]?.focus();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1057,7 +1090,7 @@ const trustStats = [
|
|||||||
name: (form.elements.namedItem('name') as HTMLInputElement)?.value.trim(),
|
name: (form.elements.namedItem('name') as HTMLInputElement)?.value.trim(),
|
||||||
email: (form.elements.namedItem('email') as HTMLInputElement)?.value.trim(),
|
email: (form.elements.namedItem('email') as HTMLInputElement)?.value.trim(),
|
||||||
phone: (form.elements.namedItem('phone') 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(),
|
message: (form.elements.namedItem('message') as HTMLTextAreaElement)?.value.trim(),
|
||||||
website: honeypot,
|
website: honeypot,
|
||||||
}),
|
}),
|
||||||
@@ -1087,7 +1120,7 @@ const trustStats = [
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Track analytics
|
// Track analytics
|
||||||
const subject = (form.elements.namedItem('subject') as HTMLSelectElement)?.value;
|
const subject = getSubjectValue();
|
||||||
trackFormSubmit('contact', success);
|
trackFormSubmit('contact', success);
|
||||||
if (success) trackEvent('contact_form_submit', { subject_category: subject });
|
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'));
|
form.querySelectorAll('[data-error]').forEach((el) => el.classList.add('hidden'));
|
||||||
if (charCount) charCount.textContent = '0 / 2000';
|
if (charCount) charCount.textContent = '0 / 2000';
|
||||||
budgetOptions.forEach((o) => o.classList.remove('selected'));
|
budgetOptions.forEach((o) => o.classList.remove('selected'));
|
||||||
|
subjectGroupEl?.classList.remove('subject-group-invalid');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -400,6 +400,52 @@ const techStack = [
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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 -->
|
<!-- 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">
|
<section class="py-24 bg-gradient-to-br from-secondary-900 via-secondary-800 to-primary-900 relative overflow-hidden">
|
||||||
<!-- Background pattern -->
|
<!-- Background pattern -->
|
||||||
|
|||||||
@@ -348,8 +348,11 @@ const featuredProjects = projects.filter(p => p.featured);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Category Filter Tabs -->
|
<!-- Category Filter Tabs
|
||||||
<div class="flex flex-wrap justify-center gap-3" role="tablist" aria-label="Filter projects by category">
|
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
|
<button
|
||||||
data-filter="all"
|
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"
|
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;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
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>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user