agent: Apply homepage CRO improvements
This commit is contained in:
+38
-163
@@ -119,15 +119,11 @@ const testimonials = [
|
||||
},
|
||||
];
|
||||
|
||||
// Stats data — canonical site-wide proof set. Keep this identical to the
|
||||
// ProofNumbers band, the /portfolio stats bar, and the /about stats grid so
|
||||
// a visitor browsing multiple pages never sees a conflicting number.
|
||||
const stats = [
|
||||
{ value: 20, suffix: '+', label: 'Projects', icon: 'briefcase' },
|
||||
{ value: 11, suffix: '', label: 'Districts Deployed', icon: 'building' },
|
||||
{ value: 50000, suffix: '+', label: 'Records Managed', icon: 'database' },
|
||||
{ value: 8, suffix: '+', label: 'Years Avg Experience', icon: 'clock' },
|
||||
];
|
||||
// NOTE: The homepage stat band is rendered once by <ProofNumbers />, which
|
||||
// holds the canonical site-wide proof set (20+ Projects · 11 Districts ·
|
||||
// 50,000+ Records · 8+ Years). The previously-duplicated "Numbers That
|
||||
// Matter" stats section (and its local `stats` array + count-up script) was
|
||||
// removed so a visitor never sees the same four numbers twice on one page.
|
||||
|
||||
// Technology stack data — relocated below the fold so social proof leads the
|
||||
// hero. Each entry pairs the framework name with a short context line and a
|
||||
@@ -212,7 +208,7 @@ const techStack = [
|
||||
</h1>
|
||||
|
||||
<p class="mt-8 text-xl sm:text-2xl text-secondary-200 max-w-3xl mx-auto opacity-0 animate-slide-up leading-relaxed" style="animation-delay: 0.6s;">
|
||||
WorkRoot designs, builds, and runs production-grade web, mobile, and ERP systems for startups, enterprises, and government bodies.
|
||||
We design, build, and run production-grade web, mobile, and ERP systems for startups, enterprises, and government bodies.
|
||||
</p>
|
||||
|
||||
<!-- Dual CTA Buttons — action verb + clear secondary -->
|
||||
@@ -236,7 +232,7 @@ const techStack = [
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
||||
</svg>
|
||||
See our work
|
||||
See 20+ case studies
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -254,6 +250,35 @@ const techStack = [
|
||||
<span>No commitment</span>
|
||||
</p>
|
||||
|
||||
<!-- Product preview — framed dashboard mock so the hero leads with a
|
||||
concrete product visual, not text alone. The image is a polished
|
||||
placeholder UI (public/images/product-dashboard.svg); swap in a
|
||||
real product screenshot or screen-recording poster frame
|
||||
(≥1200×720, .webp/.png) at the same path — no template change
|
||||
needed. Explicit width/height keep CLS at zero while it loads. -->
|
||||
<figure class="mt-16 sm:mt-20 max-w-4xl mx-auto opacity-0 animate-slide-up" style="animation-delay: 1s;">
|
||||
<div class="relative rounded-2xl border border-white/15 bg-white/5 backdrop-blur-sm shadow-2xl shadow-primary/20 overflow-hidden">
|
||||
<!-- Glow behind the frame -->
|
||||
<div class="absolute -inset-px rounded-2xl bg-gradient-to-tr from-primary-500/10 via-transparent to-accent-400/10 pointer-events-none" aria-hidden="true"></div>
|
||||
<!-- Browser chrome -->
|
||||
<div class="flex items-center gap-2 px-4 py-3 border-b border-white/10 bg-white/5" aria-hidden="true">
|
||||
<span class="w-3 h-3 rounded-full bg-red-400/80"></span>
|
||||
<span class="w-3 h-3 rounded-full bg-accent-400/80"></span>
|
||||
<span class="w-3 h-3 rounded-full bg-green-400/80"></span>
|
||||
<span class="ml-4 inline-flex items-center h-6 px-3 rounded-md bg-white/10 text-xs text-secondary-300 font-medium">app.workroot.in/dashboard</span>
|
||||
</div>
|
||||
<img
|
||||
src="/images/product-dashboard.svg"
|
||||
alt="WorkRoot project dashboard preview — delivery metrics, activity timeline, and deployment overview"
|
||||
width="1200"
|
||||
height="720"
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
class="block w-full h-auto"
|
||||
/>
|
||||
</div>
|
||||
</figure>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -426,54 +451,6 @@ const techStack = [
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Stats/Metrics Section - Enhanced -->
|
||||
<section class="py-24 bg-white dark:bg-secondary-900">
|
||||
<div class="container-wrapper">
|
||||
<div class="text-center max-w-3xl mx-auto mb-16" data-animate="fade-up">
|
||||
<span class="inline-block px-4 py-2 rounded-full bg-accent-50 dark:bg-accent-900/20 text-accent-700 dark:text-accent-300 text-sm font-bold uppercase tracking-wider mb-4">Track Record</span>
|
||||
<h2 class="text-4xl sm:text-5xl font-bold text-secondary-900 dark:text-secondary-100 mb-6">Numbers That Matter</h2>
|
||||
<p class="text-xl text-secondary-600 dark:text-secondary-400">
|
||||
Our success is measured by the impact we create for our clients.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{stats.map((stat, index) => (
|
||||
<div class="group text-center bg-gradient-to-br from-secondary-50 to-white dark:from-secondary-800 dark:to-secondary-800/80 rounded-3xl p-10 border-2 border-secondary-100 dark:border-secondary-700 hover:border-primary-400 hover:shadow-2xl transition-all duration-500 hover:-translate-y-2" data-animate="scale-up" data-delay={String(index * 100)}>
|
||||
{/* Icon */}
|
||||
<div class="w-16 h-16 rounded-2xl bg-gradient-to-br from-primary-500 to-primary-600 flex items-center justify-center mx-auto mb-6 group-hover:scale-110 group-hover:rotate-12 transition-all duration-500 shadow-lg">
|
||||
{stat.icon === 'briefcase' && (
|
||||
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
)}
|
||||
{stat.icon === 'database' && (
|
||||
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"></path>
|
||||
</svg>
|
||||
)}
|
||||
{stat.icon === 'building' && (
|
||||
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"></path>
|
||||
</svg>
|
||||
)}
|
||||
{stat.icon === 'clock' && (
|
||||
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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"></path>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div class="stat-counter text-5xl font-bold text-secondary-900 dark:text-secondary-100 mb-2" data-target={stat.value} data-suffix={stat.suffix}>
|
||||
{stat.value.toLocaleString('en-IN')}{stat.suffix}
|
||||
</div>
|
||||
<div class="text-secondary-600 dark:text-secondary-400 font-semibold">{stat.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Testimonials Section - Enhanced -->
|
||||
<section class="py-24 bg-gradient-to-b from-secondary-50 to-white dark:from-secondary-900 dark:to-secondary-900">
|
||||
<div class="container-wrapper">
|
||||
@@ -681,7 +658,7 @@ const techStack = [
|
||||
<div class="container-wrapper text-center relative z-10">
|
||||
<div class="max-w-4xl mx-auto" data-animate="fade-up">
|
||||
<h2 class="text-4xl sm:text-5xl lg:text-6xl font-bold text-white mb-8 leading-tight">
|
||||
Ready to ship your next <span class="text-transparent bg-clip-text bg-gradient-to-r from-primary-300 to-accent-400">production system?</span>
|
||||
Ready to ship in <span class="text-transparent bg-clip-text bg-gradient-to-r from-primary-300 to-accent-400">8 weeks</span> instead of 8 months?
|
||||
</h2>
|
||||
<p class="text-xl sm:text-2xl text-secondary-200 mb-10 max-w-2xl mx-auto leading-relaxed">
|
||||
Tell us about your project and we'll send back a free, no-obligation scope and timeline within one business day.
|
||||
@@ -722,7 +699,7 @@ const techStack = [
|
||||
<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="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
|
||||
</svg>
|
||||
See our work
|
||||
See 20+ case studies
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -805,67 +782,11 @@ const techStack = [
|
||||
animation: pulse-slower 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Stat counter animation */
|
||||
.stat-counter {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* (FAQ accordion styles moved into <HomepageFaq />, which now renders the
|
||||
only homepage FAQ section.) */
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Stats counter animation
|
||||
// The initial textContent already renders the final value (server-side),
|
||||
// so users with no JS / failed hydration / reduced-motion still see the
|
||||
// correct number. The animation here briefly resets to 0 and counts up.
|
||||
function animateCounters() {
|
||||
const counters = document.querySelectorAll<HTMLElement>('.stat-counter');
|
||||
const duration = 2000;
|
||||
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
|
||||
counters.forEach((counter) => {
|
||||
const target = parseInt(counter.getAttribute('data-target') || '0', 10);
|
||||
const suffix = counter.getAttribute('data-suffix') || '';
|
||||
|
||||
// Guaranteed final state — set the resolved value up front so a
|
||||
// mid-animation failure can never leave the user staring at "0".
|
||||
// Use the Indian locale grouping so large counters (e.g. 50,000+)
|
||||
// render with the same comma format as the server-rendered markup.
|
||||
const finalText = target.toLocaleString('en-IN') + suffix;
|
||||
|
||||
if (reduceMotion || !Number.isFinite(target) || target <= 0) {
|
||||
counter.textContent = finalText;
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid running the animation more than once per counter.
|
||||
if (counter.dataset.counted === 'true') return;
|
||||
counter.dataset.counted = 'true';
|
||||
|
||||
const startTime = performance.now();
|
||||
counter.textContent = (0).toLocaleString('en-IN') + suffix;
|
||||
|
||||
function updateCounter(currentTime: number) {
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
|
||||
const current = Math.floor(easeOutQuart * target);
|
||||
|
||||
counter.textContent = current.toLocaleString('en-IN') + suffix;
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(updateCounter);
|
||||
} else {
|
||||
// Snap to exact final value to avoid floor-rounding drift.
|
||||
counter.textContent = finalText;
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(updateCounter);
|
||||
});
|
||||
}
|
||||
|
||||
// Testimonials carousel
|
||||
function initCarousel() {
|
||||
const track = document.querySelector('.testimonial-track') as HTMLElement;
|
||||
@@ -946,53 +867,7 @@ const techStack = [
|
||||
// (FAQ accordion script moved into <HomepageFaq />, which now renders the
|
||||
// only homepage FAQ section and ships its own inline init.)
|
||||
|
||||
// Intersection Observer for counter animation
|
||||
function initStatsCounter() {
|
||||
const firstCounter = document.querySelector('.stat-counter');
|
||||
const statsSection = firstCounter?.closest('section');
|
||||
|
||||
// If the markup isn't there for any reason, bail silently — the
|
||||
// server-rendered numbers are already correct.
|
||||
if (!statsSection) return;
|
||||
|
||||
// Older browsers / failed feature detection: just render the final
|
||||
// values immediately. They're already in the DOM, but this also
|
||||
// protects against the path where data-target was patched at runtime.
|
||||
if (typeof IntersectionObserver === 'undefined') {
|
||||
animateCounters();
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
animateCounters();
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
},
|
||||
// Lower threshold so the animation fires reliably on tall mobile
|
||||
// viewports; rootMargin lets it kick in just before the section
|
||||
// scrolls into view for a smoother first impression.
|
||||
{ threshold: 0.1, rootMargin: '0px 0px -10% 0px' }
|
||||
);
|
||||
|
||||
observer.observe(statsSection);
|
||||
|
||||
// If the section is already in the viewport at load time the
|
||||
// observer fires on its next tick, but we also guard with a fallback
|
||||
// so anyone who lands mid-scroll never sees the static "0" path.
|
||||
const rect = statsSection.getBoundingClientRect();
|
||||
const inView = rect.top < window.innerHeight && rect.bottom > 0;
|
||||
if (inView) {
|
||||
animateCounters();
|
||||
observer.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
function bootHomepageScripts() {
|
||||
initStatsCounter();
|
||||
initCarousel();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user