First Init
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

This commit is contained in:
2026-03-21 16:46:46 +05:30
commit d402256547
216 changed files with 48375 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
---
/**
* Analytics Component
*
* Supports two analytics providers (configure via environment variables):
* - Google Analytics 4: Set GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX
* - Plausible Analytics: Set PLAUSIBLE_DOMAIN=workroot.in
*
* If neither is set, no analytics scripts are injected.
* The component respects Do Not Track (DNT) browser settings.
*/
const gaId = import.meta.env.GOOGLE_ANALYTICS_ID;
const plausibleDomain = import.meta.env.PLAUSIBLE_DOMAIN;
const hasGA4 = gaId && gaId.startsWith('G-');
const hasPlausible = !!plausibleDomain;
---
{hasGA4 && (
<>
<!-- Google Analytics 4 -->
<link rel="preconnect" href="https://www.googletagmanager.com" />
<link rel="dns-prefetch" href="https://www.google-analytics.com" />
<script
async
src={`https://www.googletagmanager.com/gtag/js?id=${gaId}`}
></script>
<script define:vars={{ gaId }}>
// Respect Do Not Track
if (navigator.doNotTrack !== '1' && window.doNotTrack !== '1') {
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', gaId, {
// Anonymize IP for GDPR compliance
anonymize_ip: true,
// Disable ad personalization signals
allow_google_signals: false,
allow_ad_personalization_signals: false,
// Send page views automatically
send_page_view: true,
});
// Expose gtag globally for event tracking
window.gtag = gtag;
}
</script>
</>
)}
{hasPlausible && (
<>
<!-- Plausible Analytics (privacy-focused) -->
<link rel="preconnect" href="https://plausible.io" />
<script
defer
data-domain={plausibleDomain}
src="https://plausible.io/js/script.tagged-events.js"
></script>
<script>
// Expose plausible event helper globally
window.plausible = window.plausible || function() {
(window.plausible.q = window.plausible.q || []).push(arguments);
};
</script>
</>
)}
{!hasGA4 && !hasPlausible && (
<!-- Analytics not configured. Set GOOGLE_ANALYTICS_ID or PLAUSIBLE_DOMAIN in .env -->
)}
+265
View File
@@ -0,0 +1,265 @@
---
// Footer Component for WorkRoot IT Solutions
// Features: Company info, quick links, services, social icons, newsletter subscription
interface Props {
class?: string;
}
const { class: className } = Astro.props;
const quickLinks = [
{ name: 'Home', href: '/' },
{ name: 'About Us', href: '/about' },
{ name: 'Services', href: '/services' },
{ name: 'Portfolio', href: '/portfolio' },
{ name: 'Blog', href: '/blog' },
{ name: 'Contact', href: '/contact' },
];
const services = [
{ name: 'Web Development', href: '/services/web-development' },
{ name: 'Mobile Apps', href: '/services/mobile-apps' },
{ name: 'Cloud Solutions', href: '/services/cloud-solutions' },
{ name: 'IT Consulting', href: '/services/it-consulting' },
{ name: 'Cybersecurity', href: '/services/cybersecurity' },
{ name: 'DevOps', href: '/services/devops' },
];
const socialLinks = [
{ name: 'LinkedIn', href: 'https://linkedin.com/company/workroot', icon: 'linkedin' },
{ name: 'Twitter', href: 'https://twitter.com/workroot', icon: 'twitter' },
{ name: 'GitHub', href: 'https://github.com/workroot', icon: 'github' },
{ name: 'Facebook', href: 'https://facebook.com/workroot', icon: 'facebook' },
];
const currentYear = new Date().getFullYear();
---
<footer class:list={['bg-secondary-900 text-secondary-200', className]}>
<!-- Main Footer Content -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 lg:py-16">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 lg:gap-12">
<!-- Company Info -->
<div class="lg:col-span-1">
<a href="/" class="inline-flex items-center gap-2 mb-4 group" aria-label="WorkRoot IT Solutions - Home">
<div class="w-10 h-10 bg-primary rounded-lg flex items-center justify-center transition-transform group-hover:scale-105" aria-hidden="true">
<span class="text-white font-bold text-lg">W</span>
</div>
<span class="text-xl font-bold text-white">WorkRoot</span>
</a>
<p class="text-secondary-400 text-sm leading-relaxed mb-6">
Empowering businesses with innovative IT solutions. We build scalable,
secure, and future-ready digital experiences.
</p>
<!-- Social Icons -->
<div class="flex items-center gap-3">
{socialLinks.map((social) => (
<a
href={social.href}
target="_blank"
rel="noopener noreferrer"
aria-label={social.name}
class="w-10 h-10 rounded-lg bg-secondary-800 flex items-center justify-center text-secondary-400 hover:bg-primary hover:text-white transition-all duration-200 hover:scale-105"
>
{social.icon === 'linkedin' && (
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
</svg>
)}
{social.icon === 'twitter' && (
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<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" aria-hidden="true">
<path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"/>
</svg>
)}
{social.icon === 'facebook' && (
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path fill-rule="evenodd" d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z" clip-rule="evenodd"/>
</svg>
)}
</a>
))}
</div>
</div>
<!-- Quick Links -->
<div>
<h3 class="text-white font-semibold text-lg mb-4">Quick Links</h3>
<ul class="space-y-3">
{quickLinks.map((link) => (
<li>
<a
href={link.href}
class="text-secondary-400 hover:text-primary transition-colors duration-200 text-sm inline-flex items-center gap-1 group"
>
<svg class="w-3 h-3 opacity-0 -translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-200" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
<span>{link.name}</span>
</a>
</li>
))}
</ul>
</div>
<!-- Services -->
<div>
<h3 class="text-white font-semibold text-lg mb-4">Our Services</h3>
<ul class="space-y-3">
{services.map((service) => (
<li>
<a
href={service.href}
class="text-secondary-400 hover:text-primary transition-colors duration-200 text-sm inline-flex items-center gap-1 group"
>
<svg class="w-3 h-3 opacity-0 -translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-200" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
<span>{service.name}</span>
</a>
</li>
))}
</ul>
</div>
<!-- Newsletter Subscription -->
<div>
<h3 class="text-white font-semibold text-lg mb-4">Stay Updated</h3>
<p class="text-secondary-400 text-sm mb-4">
Subscribe to our newsletter for the latest tech insights and company updates.
</p>
<form id="newsletter-form" class="space-y-3">
<div class="relative">
<label for="newsletter-email" class="sr-only">Email address</label>
<input
type="email"
name="email"
id="newsletter-email"
placeholder="Enter your email"
required
autocomplete="email"
aria-required="true"
aria-describedby="newsletter-message"
class="w-full px-4 py-3 bg-secondary-800 border border-secondary-700 rounded-lg text-white placeholder-secondary-500 text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all duration-200"
/>
</div>
<button
type="submit"
class="w-full px-4 py-3 bg-primary text-white font-medium rounded-lg hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 focus:ring-offset-secondary-900 transition-all duration-200 text-sm flex items-center justify-center gap-2 group"
>
<span>Subscribe</span>
<svg class="w-4 h-4 group-hover:translate-x-1 transition-transform duration-200" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"/>
</svg>
</button>
<!-- Success/Error Messages -->
<div id="newsletter-message" class="hidden text-sm py-2 px-3 rounded-lg" role="status" aria-live="polite"></div>
</form>
<p class="text-secondary-500 text-xs mt-3">
By subscribing, you agree to our
<a href="/privacy" class="text-primary hover:underline">Privacy Policy</a>.
</p>
</div>
</div>
</div>
<!-- Bottom Bar -->
<div class="border-t border-secondary-800">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div class="flex flex-col md:flex-row justify-between items-center gap-4">
<p class="text-secondary-500 text-sm text-center md:text-left">
&copy; {currentYear} WorkRoot IT Solutions. All rights reserved.
</p>
<div class="flex items-center gap-6">
<a href="/privacy" class="text-secondary-500 hover:text-primary text-sm transition-colors duration-200">
Privacy Policy
</a>
<a href="/terms" class="text-secondary-500 hover:text-primary text-sm transition-colors duration-200">
Terms of Service
</a>
<a href="/cookies" class="text-secondary-500 hover:text-primary text-sm transition-colors duration-200">
Cookie Policy
</a>
</div>
</div>
</div>
</div>
</footer>
<script>
import { trackNewsletterSignup } from '../utils/analytics';
// Newsletter form handling
const form = document.getElementById('newsletter-form') as HTMLFormElement;
const messageEl = document.getElementById('newsletter-message') as HTMLDivElement;
form?.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(form);
const email = formData.get('email') as string;
// Show loading state
const submitBtn = form.querySelector('button[type="submit"]') as HTMLButtonElement;
const originalContent = submitBtn.innerHTML;
submitBtn.disabled = true;
submitBtn.innerHTML = `
<svg class="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
<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>
<span>Subscribing...</span>
`;
try {
const response = await fetch('/api/newsletter', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
const data = await response.json() as { success: boolean; message?: string; error?: string };
if (response.ok && data.success) {
trackNewsletterSignup(true);
// Show success message
messageEl.classList.remove('hidden', 'bg-red-900/50', 'text-red-300');
messageEl.classList.add('bg-green-900/50', 'text-green-300');
messageEl.textContent = data.message ?? 'Thanks for subscribing! Check your inbox to confirm.';
// Reset form on success only
form.reset();
} else {
trackNewsletterSignup(false);
// Show error message from server
messageEl.classList.remove('hidden', 'bg-green-900/50', 'text-green-300');
messageEl.classList.add('bg-red-900/50', 'text-red-300');
messageEl.textContent = data.error ?? 'Something went wrong. Please try again.';
}
} catch {
trackNewsletterSignup(false);
// Network error
messageEl.classList.remove('hidden', 'bg-green-900/50', 'text-green-300');
messageEl.classList.add('bg-red-900/50', 'text-red-300');
messageEl.textContent = 'Network error. Please check your connection and try again.';
} finally {
// Restore button
submitBtn.disabled = false;
submitBtn.innerHTML = originalContent;
// Hide message after 5 seconds
setTimeout(() => {
messageEl.classList.add('hidden');
}, 5000);
}
});
</script>
+325
View File
@@ -0,0 +1,325 @@
---
// Header Component for WorkRoot IT Solutions
// Features: Sticky header, responsive nav, mobile hamburger menu, active link highlighting
interface Props {
class?: string;
}
const { class: className } = Astro.props;
const navLinks = [
{ name: 'Home', href: '/' },
{ name: 'About', href: '/about' },
{ name: 'Services', href: '/services' },
{ name: 'Portfolio', href: '/portfolio' },
{ name: 'Blog', href: '/blog' },
{ name: 'Contact', href: '/contact' },
];
const currentPath = Astro.url.pathname;
---
<header
class:list={[
'fixed top-0 left-0 right-0 z-50 bg-white/95 backdrop-blur-sm border-b border-secondary-100 transition-shadow duration-300',
className,
]}
id="main-header"
>
<div class="container-wrapper">
<nav class="flex items-center justify-between h-16 lg:h-20" aria-label="Main navigation">
<!-- Logo -->
<a href="/" class="flex items-center gap-2 group" aria-label="WorkRoot IT Solutions - Home">
<div class="w-10 h-10 bg-primary rounded-lg flex items-center justify-center transition-transform group-hover:scale-105">
<span class="text-white font-bold text-xl">W</span>
</div>
<div class="hidden sm:block">
<span class="font-bold text-xl text-secondary">WorkRoot</span>
<span class="text-primary font-semibold text-sm block -mt-1">IT Solutions</span>
</div>
</a>
<!-- Desktop Navigation -->
<ul class="hidden lg:flex items-center gap-1" role="menubar">
{navLinks.map((link) => {
const isActive = currentPath === link.href ||
(link.href !== '/' && currentPath.startsWith(link.href));
return (
<li role="none">
<a
href={link.href}
role="menuitem"
data-astro-prefetch="hover"
class:list={[
'relative px-4 py-2 text-sm font-medium transition-colors rounded-lg',
isActive
? 'text-primary bg-primary-50'
: 'text-secondary-600 hover:text-primary hover:bg-secondary-50',
]}
aria-current={isActive ? 'page' : undefined}
>
{link.name}
{isActive && (
<span class="absolute bottom-0 left-1/2 -translate-x-1/2 w-1 h-1 bg-primary rounded-full" />
)}
</a>
</li>
);
})}
</ul>
<!-- Desktop CTA -->
<div class="hidden lg:flex items-center gap-4">
<a
href="/contact"
class="btn-primary text-sm"
>
Get Started
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</a>
</div>
<!-- Mobile Menu Button -->
<button
type="button"
class="lg:hidden relative w-10 h-10 flex items-center justify-center text-secondary-600 hover:text-primary 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>
</nav>
</div>
<!-- Mobile Menu Overlay -->
<div
id="mobile-menu-overlay"
class="fixed inset-0 bg-secondary-900/50 backdrop-blur-sm opacity-0 pointer-events-none transition-opacity duration-300 lg:hidden"
aria-hidden="true"
></div>
<!-- Mobile Menu Panel -->
<div
id="mobile-menu"
class="fixed top-0 right-0 h-full w-80 max-w-[85vw] bg-white shadow-2xl transform translate-x-full transition-transform duration-300 ease-out lg:hidden"
role="dialog"
aria-modal="true"
aria-label="Mobile navigation menu"
>
<div class="flex flex-col h-full">
<!-- Mobile Menu Header -->
<div class="flex items-center justify-between p-4 border-b border-secondary-100">
<a href="/" class="flex items-center gap-2" aria-label="WorkRoot IT Solutions - Home">
<div class="w-8 h-8 bg-primary rounded-lg flex items-center justify-center" aria-hidden="true">
<span class="text-white font-bold text-lg">W</span>
</div>
<span class="font-bold text-lg text-secondary">WorkRoot</span>
</a>
<button
type="button"
class="w-10 h-10 flex items-center justify-center text-secondary-500 hover:text-secondary-700 transition-colors"
id="mobile-menu-close"
aria-label="Close menu"
>
<svg class="w-6 h-6" 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>
</button>
</div>
<!-- Mobile Navigation Links -->
<nav class="flex-1 overflow-y-auto py-4">
<ul class="space-y-1 px-3" role="menu">
{navLinks.map((link, index) => {
const isActive = currentPath === link.href ||
(link.href !== '/' && currentPath.startsWith(link.href));
return (
<li role="none" style={`--delay: ${index * 50}ms`}>
<a
href={link.href}
role="menuitem"
data-astro-prefetch="hover"
class:list={[
'mobile-nav-link flex items-center gap-3 px-4 py-3 rounded-lg text-base font-medium transition-all duration-200',
isActive
? 'text-primary bg-primary-50'
: 'text-secondary-700 hover:text-primary hover:bg-secondary-50',
]}
aria-current={isActive ? 'page' : undefined}
>
{link.name}
{isActive && (
<span class="ml-auto w-2 h-2 bg-primary rounded-full" />
)}
</a>
</li>
);
})}
</ul>
</nav>
<!-- Mobile Menu Footer -->
<div class="p-4 border-t border-secondary-100">
<a
href="/contact"
class="btn-primary w-full justify-center"
>
Get Started
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</a>
<p class="text-center text-sm text-secondary-500 mt-4">
Need help? <a href="tel:+1234567890" class="text-primary hover:underline">Call us</a>
</p>
</div>
</div>
</div>
</header>
<!-- Spacer for fixed header -->
<div class="h-16 lg:h-20" aria-hidden="true"></div>
<style>
/* Hamburger Icon Styles */
.hamburger-icon {
width: 24px;
height: 18px;
position: relative;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.hamburger-line {
display: block;
width: 100%;
height: 2px;
background-color: currentColor;
border-radius: 1px;
transition: all 0.3s ease;
transform-origin: center;
}
/* Hamburger to X animation */
.menu-open .hamburger-line-1 {
transform: translateY(8px) rotate(45deg);
}
.menu-open .hamburger-line-2 {
opacity: 0;
transform: scaleX(0);
}
.menu-open .hamburger-line-3 {
transform: translateY(-8px) rotate(-45deg);
}
/* Mobile nav link animation */
.mobile-nav-link {
opacity: 0;
transform: translateX(20px);
transition: opacity 0.3s ease, transform 0.3s ease;
transition-delay: var(--delay, 0ms);
}
.menu-open .mobile-nav-link {
opacity: 1;
transform: translateX(0);
}
/* Header shadow on scroll */
.header-scrolled {
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
}
</style>
<script>
// Mobile Menu Toggle
const mobileMenuToggle = document.getElementById('mobile-menu-toggle');
const mobileMenuClose = document.getElementById('mobile-menu-close');
const mobileMenu = document.getElementById('mobile-menu');
const mobileMenuOverlay = document.getElementById('mobile-menu-overlay');
const header = document.getElementById('main-header');
let isMenuOpen = false;
function openMenu() {
isMenuOpen = true;
mobileMenu?.classList.remove('translate-x-full');
mobileMenuOverlay?.classList.remove('opacity-0', 'pointer-events-none');
mobileMenuToggle?.classList.add('menu-open');
mobileMenuToggle?.setAttribute('aria-expanded', 'true');
document.body.style.overflow = 'hidden';
// Trigger link animations
document.querySelectorAll('.mobile-nav-link').forEach((link) => {
link.closest('li')?.classList.add('menu-open');
});
}
function closeMenu() {
isMenuOpen = false;
mobileMenu?.classList.add('translate-x-full');
mobileMenuOverlay?.classList.add('opacity-0', 'pointer-events-none');
mobileMenuToggle?.classList.remove('menu-open');
mobileMenuToggle?.setAttribute('aria-expanded', 'false');
document.body.style.overflow = '';
// Reset link animations
document.querySelectorAll('.mobile-nav-link').forEach((link) => {
link.closest('li')?.classList.remove('menu-open');
});
}
mobileMenuToggle?.addEventListener('click', () => {
if (isMenuOpen) {
closeMenu();
} else {
openMenu();
}
});
mobileMenuClose?.addEventListener('click', closeMenu);
mobileMenuOverlay?.addEventListener('click', closeMenu);
// Close menu on escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && isMenuOpen) {
closeMenu();
}
});
// Close menu on window resize (if transitioning to desktop)
window.addEventListener('resize', () => {
if (window.innerWidth >= 1024 && isMenuOpen) {
closeMenu();
}
});
// Header shadow on scroll
let lastScrollY = 0;
window.addEventListener('scroll', () => {
const currentScrollY = window.scrollY;
if (currentScrollY > 10) {
header?.classList.add('header-scrolled');
} else {
header?.classList.remove('header-scrolled');
}
lastScrollY = currentScrollY;
}, { passive: true });
</script>
+87
View File
@@ -0,0 +1,87 @@
---
/**
* LazyImage Component
*
* Lightweight image component with:
* - Native lazy loading
* - Placeholder background while loading
* - Aspect ratio preservation (prevents CLS)
* - Fade-in animation on load
*
* Use for placeholder/decorative images or when full Astro optimization isn't needed.
*/
interface Props {
src: string;
alt: string;
width: number;
height: number;
class?: string;
loading?: 'lazy' | 'eager';
fetchpriority?: 'high' | 'low' | 'auto';
placeholder?: string; // Background color while loading
}
const {
src,
alt,
width,
height,
class: className = '',
loading = 'lazy',
fetchpriority = 'auto',
placeholder = '#1e293b', // secondary-800
} = Astro.props;
const aspectRatio = (height / width) * 100;
---
<div
class:list={['lazy-image-wrapper', className]}
style={`--aspect-ratio: ${aspectRatio}%; --placeholder-bg: ${placeholder};`}
>
<img
src={src}
alt={alt}
width={width}
height={height}
loading={loading}
decoding="async"
fetchpriority={fetchpriority}
class="lazy-image"
onload="this.classList.add('loaded')"
/>
</div>
<style>
.lazy-image-wrapper {
position: relative;
width: 100%;
padding-bottom: var(--aspect-ratio);
background-color: var(--placeholder-bg);
overflow: hidden;
}
.lazy-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0;
transition: opacity 0.3s ease-in-out;
}
.lazy-image.loaded {
opacity: 1;
}
/* Disable transition for users who prefer reduced motion */
@media (prefers-reduced-motion: reduce) {
.lazy-image {
transition: none;
opacity: 1;
}
}
</style>
+119
View File
@@ -0,0 +1,119 @@
---
/**
* OptimizedImage Component
*
* Features:
* - Automatic WebP conversion via Astro's image service
* - Native lazy loading with fallback
* - Responsive srcset generation
* - Blur placeholder support
* - Proper aspect ratio to prevent CLS
*/
import { Image } from 'astro:assets';
interface Props {
src: ImageMetadata | string;
alt: string;
width?: number;
height?: number;
class?: string;
loading?: 'lazy' | 'eager';
decoding?: 'async' | 'sync' | 'auto';
fetchpriority?: 'high' | 'low' | 'auto';
sizes?: string;
widths?: number[];
quality?: number;
format?: 'webp' | 'avif' | 'png' | 'jpg';
placeholder?: 'blur' | 'none';
}
const {
src,
alt,
width,
height,
class: className = '',
loading = 'lazy',
decoding = 'async',
fetchpriority = 'auto',
sizes = '(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw',
widths = [320, 640, 960, 1280, 1920],
quality = 80,
format = 'webp',
placeholder = 'none',
} = Astro.props;
// Check if src is a remote URL string
const isRemote = typeof src === 'string' && (src.startsWith('http') || src.startsWith('//'));
---
{isRemote ? (
<img
src={src}
alt={alt}
width={width}
height={height}
class={className}
loading={loading}
decoding={decoding}
fetchpriority={fetchpriority}
style={width && height ? `aspect-ratio: ${width}/${height}` : undefined}
/>
) : typeof src === 'string' ? (
<!-- Local path string - use native img with lazy loading -->
<img
src={src}
alt={alt}
width={width}
height={height}
class={className}
loading={loading}
decoding={decoding}
fetchpriority={fetchpriority}
style={width && height ? `aspect-ratio: ${width}/${height}` : undefined}
/>
) : (
<!-- ImageMetadata - use Astro's optimized Image component -->
<Image
src={src}
alt={alt}
width={width || src.width}
height={height || src.height}
class={className}
loading={loading}
decoding={decoding}
format={format}
quality={quality}
widths={widths}
sizes={sizes}
/>
)}
<style>
img {
/* Prevent layout shift by maintaining aspect ratio */
height: auto;
max-width: 100%;
}
/* Add subtle fade-in effect for lazy loaded images */
img[loading='lazy'] {
opacity: 0;
animation: fadeIn 0.3s ease-in-out forwards;
}
@keyframes fadeIn {
to {
opacity: 1;
}
}
/* Disable animation for users who prefer reduced motion */
@media (prefers-reduced-motion: reduce) {
img[loading='lazy'] {
animation: none;
opacity: 1;
}
}
</style>
+161
View File
@@ -0,0 +1,161 @@
---
// PWA Install Prompt
// Shows a custom install banner when the browser fires 'beforeinstallprompt'
// Also provides a manual install button accessible via #pwa-install-btn
---
<!-- Install Prompt Banner -->
<div
id="pwa-install-banner"
role="banner"
aria-label="Install WorkRoot app"
class="hidden fixed bottom-4 left-4 right-4 sm:left-auto sm:right-4 sm:w-80 z-50 bg-white border border-secondary-200 rounded-xl shadow-xl p-4"
>
<div class="flex items-start gap-3">
<img
src="/apple-touch-icon.png"
alt="WorkRoot"
width="44"
height="44"
class="rounded-lg flex-shrink-0"
loading="lazy"
/>
<div class="flex-1 min-w-0">
<p class="font-semibold text-secondary-900 text-sm">Install WorkRoot</p>
<p class="text-secondary-500 text-xs mt-0.5">
Add to your home screen for quick access
</p>
</div>
<button
id="pwa-dismiss-btn"
aria-label="Dismiss install prompt"
class="flex-shrink-0 text-secondary-400 hover:text-secondary-600 transition-colors"
>
<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="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="flex gap-2 mt-3">
<button
id="pwa-install-btn"
class="flex-1 bg-primary text-white text-sm font-semibold py-2 px-4 rounded-lg hover:bg-primary/90 transition-colors"
>
Install
</button>
<button
id="pwa-later-btn"
class="flex-1 bg-secondary-100 text-secondary-700 text-sm font-semibold py-2 px-4 rounded-lg hover:bg-secondary-200 transition-colors"
>
Not now
</button>
</div>
</div>
<script>
// ─── Background Sync Helper ──────────────────────────────────────────────────
// Exposed globally so form pages can queue submissions
window.queueFormSync = async function (url, method, body, formType) {
const db = await openDB();
await addToDB(db, 'workroot-sync-queue', {
url,
method,
body,
formType,
timestamp: Date.now(),
});
// Register background sync if supported
if ('serviceWorker' in navigator && 'SyncManager' in window) {
const reg = await navigator.serviceWorker.ready;
await reg.sync.register('form-sync');
}
};
function openDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('workroot-pwa', 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains('workroot-sync-queue')) {
const store = db.createObjectStore('workroot-sync-queue', { keyPath: 'id', autoIncrement: true });
store.createIndex('timestamp', 'timestamp', { unique: false });
}
};
request.onsuccess = (event) => resolve(event.target.result);
request.onerror = () => reject(request.error);
});
}
function addToDB(db, storeName, item) {
return new Promise((resolve, reject) => {
const tx = db.transaction(storeName, 'readwrite');
const store = tx.objectStore(storeName);
const request = store.add(item);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// ─── Install Prompt ───────────────────────────────────────────────────────────
let deferredPrompt = null;
const banner = document.getElementById('pwa-install-banner');
const installBtn = document.getElementById('pwa-install-btn');
const dismissBtn = document.getElementById('pwa-dismiss-btn');
const laterBtn = document.getElementById('pwa-later-btn');
// Don't show banner if already installed or dismissed within 7 days
const DISMISSED_KEY = 'pwa-install-dismissed';
const dismissedAt = localStorage.getItem(DISMISSED_KEY);
const sevenDays = 7 * 24 * 60 * 60 * 1000;
const recentlyDismissed = dismissedAt && Date.now() - parseInt(dismissedAt) < sevenDays;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
if (!recentlyDismissed) {
// Delay banner so it doesn't distract on first page load
setTimeout(() => {
banner?.classList.remove('hidden');
}, 4000);
}
});
installBtn?.addEventListener('click', async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
deferredPrompt = null;
banner?.classList.add('hidden');
if (outcome === 'accepted') {
localStorage.removeItem(DISMISSED_KEY);
}
});
function dismissBanner() {
banner?.classList.add('hidden');
localStorage.setItem(DISMISSED_KEY, String(Date.now()));
deferredPrompt = null;
}
dismissBtn?.addEventListener('click', dismissBanner);
laterBtn?.addEventListener('click', dismissBanner);
// Hide banner once app is installed
window.addEventListener('appinstalled', () => {
banner?.classList.add('hidden');
deferredPrompt = null;
});
// ─── SW Sync Success Listener ─────────────────────────────────────────────────
if ('serviceWorker' in navigator) {
navigator.serviceWorker.addEventListener('message', (event) => {
if (event.data?.type === 'SYNC_SUCCESS') {
// Dispatch custom event so form handlers can react
window.dispatchEvent(new CustomEvent('pwa:sync-success', { detail: event.data }));
}
});
}
</script>
+201
View File
@@ -0,0 +1,201 @@
---
// SEO Component for page-specific structured data
// Use in the head slot of BaseLayout for additional schema markup
interface BreadcrumbItem {
name: string;
url: string;
}
interface Props {
type?: 'WebPage' | 'BlogPosting' | 'Article' | 'Service' | 'AboutPage' | 'ContactPage' | 'FAQPage';
breadcrumbs?: BreadcrumbItem[];
article?: {
author: string;
datePublished: string;
dateModified?: string;
category?: string;
tags?: string[];
image?: string;
wordCount?: number;
};
service?: {
name: string;
description: string;
};
faq?: Array<{
question: string;
answer: string;
}>;
}
const { type = 'WebPage', breadcrumbs, article, service, faq } = Astro.props;
const baseUrl = 'https://workroot.in';
const currentUrl = Astro.url.href;
// Get page title and description from parent layout props
const pageTitle = (Astro.props as any).title || 'WorkRoot IT Solutions';
const pageDescription = (Astro.props as any).description || 'Professional IT services including web development, cloud solutions, and digital transformation for businesses.';
// Generate BreadcrumbList schema
const breadcrumbSchema = breadcrumbs ? {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": breadcrumbs.map((item, index) => ({
"@type": "ListItem",
"position": index + 1,
"name": item.name,
"item": `${baseUrl}${item.url}`
}))
} : null;
// Generate BlogPosting schema for blog posts (more specific than Article)
const blogPostingSchema = article && type === 'BlogPosting' ? {
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": pageTitle,
"description": pageDescription,
"image": article.image ? {
"@type": "ImageObject",
"url": article.image,
"width": 1200,
"height": 675
} : undefined,
"author": {
"@type": "Person",
"name": article.author,
"url": `${baseUrl}/about`
},
"datePublished": article.datePublished,
"dateModified": article.dateModified || article.datePublished,
"publisher": {
"@type": "Organization",
"name": "WorkRoot IT Solutions",
"url": baseUrl,
"logo": {
"@type": "ImageObject",
"url": `${baseUrl}/logo.png`,
"width": 512,
"height": 512
}
},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": currentUrl
},
"keywords": article.tags?.join(', '),
"articleSection": article.category,
"wordCount": article.wordCount,
"inLanguage": "en-US"
} : null;
// Generate Service schema
const serviceSchema = service ? {
"@context": "https://schema.org",
"@type": "Service",
"name": service.name,
"description": service.description,
"provider": {
"@type": "Organization",
"name": "WorkRoot IT Solutions",
"url": baseUrl
},
"areaServed": "Worldwide",
"serviceType": service.name
} : null;
// Generate WebPage schema for standard pages
const webPageSchema = type === 'WebPage' && Astro.url.pathname !== '/' ? {
"@context": "https://schema.org",
"@type": "WebPage",
"name": pageTitle,
"description": pageDescription,
"url": currentUrl,
"inLanguage": "en-US",
"isPartOf": {
"@type": "WebSite",
"@id": `${baseUrl}/#website`
},
"about": {
"@type": "Organization",
"name": "WorkRoot IT Solutions"
},
"primaryImageOfPage": {
"@type": "ImageObject",
"url": `${baseUrl}/og-image.jpg`
}
} : null;
// Generate FAQPage schema
const faqPageSchema = faq && faq.length > 0 ? {
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": faq.map((item) => ({
"@type": "Question",
"name": item.question,
"acceptedAnswer": {
"@type": "Answer",
"text": item.answer
}
}))
} : null;
// Generate AboutPage schema
const aboutPageSchema = type === 'AboutPage' ? {
"@context": "https://schema.org",
"@type": "AboutPage",
"name": pageTitle,
"description": pageDescription,
"url": currentUrl,
"inLanguage": "en-US",
"mainEntity": {
"@type": "Organization",
"name": "WorkRoot IT Solutions",
"url": baseUrl
}
} : null;
// Generate ContactPage schema
const contactPageSchema = type === 'ContactPage' ? {
"@context": "https://schema.org",
"@type": "ContactPage",
"name": pageTitle,
"description": pageDescription,
"url": currentUrl,
"inLanguage": "en-US",
"mainEntity": {
"@type": "Organization",
"name": "WorkRoot IT Solutions",
"url": baseUrl
}
} : null;
---
{breadcrumbSchema && (
<script type="application/ld+json" set:html={JSON.stringify(breadcrumbSchema)} />
)}
{blogPostingSchema && (
<script type="application/ld+json" set:html={JSON.stringify(blogPostingSchema)} />
)}
{serviceSchema && (
<script type="application/ld+json" set:html={JSON.stringify(serviceSchema)} />
)}
{webPageSchema && (
<script type="application/ld+json" set:html={JSON.stringify(webPageSchema)} />
)}
{faqPageSchema && (
<script type="application/ld+json" set:html={JSON.stringify(faqPageSchema)} />
)}
{aboutPageSchema && (
<script type="application/ld+json" set:html={JSON.stringify(aboutPageSchema)} />
)}
{contactPageSchema && (
<script type="application/ld+json" set:html={JSON.stringify(contactPageSchema)} />
)}
+42
View File
@@ -0,0 +1,42 @@
---
/**
* Badge / Eyebrow Label Component
*
* Used as section labels ("Why Choose Us", "Our Services") above headings.
*
* @prop {string} label - The badge text
* @prop {'primary' | 'primary-dark' | 'accent' | 'secondary'} variant - Color variant
* - 'primary' → light background (use on white / light sections)
* - 'primary-dark' → translucent (use on dark/hero sections)
* - 'accent' → amber tones (testimonials, stats)
* - 'secondary' → neutral gray
* @prop {string} [class] - Extra classes to merge in
*/
interface Props {
label: string;
variant?: 'primary' | 'primary-dark' | 'accent' | 'secondary';
class?: string;
}
const {
label,
variant = 'primary',
class: extraClass = '',
} = Astro.props;
const variantClasses: Record<string, string> = {
'primary': 'bg-primary-50 text-primary-700',
'primary-dark': 'bg-primary-400/20 text-primary-300',
'accent': 'bg-accent-50 text-accent-700',
'secondary': 'bg-secondary-100 text-secondary-700',
};
const classes = [
'inline-block px-4 py-2 rounded-full text-sm font-bold uppercase tracking-wider mb-4',
variantClasses[variant],
extraClass,
].filter(Boolean).join(' ');
---
<span class={classes}>{label}</span>
+80
View File
@@ -0,0 +1,80 @@
---
/**
* Card Component
*
* A versatile card wrapper with predefined style variants matching
* the WorkRoot design system.
*
* @prop {'feature' | 'service' | 'surface' | 'dark' | 'plain'} variant
* - 'feature' → gradient bg, hover lift + border glow (benefits grid)
* - 'service' → white, rounded-3xl, heavy shadow on hover (services grid)
* - 'surface' → secondary-50 bg, subtle hover shadow (contact info, small cards)
* - 'dark' → white/5 glassmorphism on dark backgrounds (how-it-works steps)
* - 'plain' → no preset styles, just the slot (full custom)
* @prop {string} [padding] - Override default padding (e.g. 'p-6')
* @prop {string} [class] - Extra classes to merge in
*/
interface Props {
variant?: 'feature' | 'service' | 'surface' | 'dark' | 'plain';
padding?: string;
class?: string;
}
const {
variant = 'feature',
padding,
class: extraClass = '',
} = Astro.props;
const variantClasses: Record<string, string> = {
'feature': [
'group relative bg-gradient-to-br from-secondary-50 to-white',
'rounded-2xl border border-secondary-100',
'hover:border-primary/30 hover:shadow-2xl',
'transition-all duration-500 hover:-translate-y-2',
].join(' '),
'service': [
'group relative bg-white rounded-3xl shadow-lg',
'border-2 border-secondary-100',
'hover:border-primary-400 hover:shadow-2xl',
'transition-all duration-500 overflow-hidden',
].join(' '),
'surface': [
'bg-secondary-50 rounded-xl',
'border border-secondary-200',
'hover:shadow-lg transition-shadow duration-300',
].join(' '),
'dark': [
'group relative bg-white/5 backdrop-blur-sm rounded-2xl',
'border border-white/10',
'hover:bg-white/10 hover:border-primary-400/50',
'transition-all duration-500',
].join(' '),
'plain': '',
};
const defaultPadding: Record<string, string> = {
'feature': 'p-8',
'service': 'p-10',
'surface': 'p-6',
'dark': 'p-8',
'plain': '',
};
const paddingClass = padding ?? defaultPadding[variant];
const classes = [
variantClasses[variant],
paddingClass,
extraClass,
].filter(Boolean).join(' ');
---
<div class={classes}>
<slot />
</div>
+72
View File
@@ -0,0 +1,72 @@
---
/**
* SectionHeader Component
*
* Renders the standardized eyebrow + heading + description pattern
* used in every section of the WorkRoot site.
*
* @prop {string} badge - Eyebrow label text (e.g. "Why Choose Us")
* @prop {string} title - Section heading
* @prop {string} [description] - Optional supporting paragraph
* @prop {'light' | 'dark'} [theme] - 'light' for white/gray sections, 'dark' for hero-style sections
* @prop {'primary' | 'accent'} [badgeColor] - Badge color when theme is 'light'
* @prop {string} [maxWidth] - Max width class for the container (default 'max-w-3xl')
* @prop {string} [class] - Extra classes
*/
interface Props {
badge: string;
title: string;
description?: string;
theme?: 'light' | 'dark';
badgeColor?: 'primary' | 'accent';
maxWidth?: string;
class?: string;
}
const {
badge,
title,
description,
theme = 'light',
badgeColor = 'primary',
maxWidth = 'max-w-3xl',
class: extraClass = '',
} = Astro.props;
const isLight = theme === 'light';
const badgeClasses: Record<string, string> = {
'primary-light': 'bg-primary-50 text-primary-700',
'accent-light': 'bg-accent-50 text-accent-700',
'primary-dark': 'bg-primary-400/20 text-primary-300',
'accent-dark': 'bg-accent-400/20 text-accent-300',
};
const badgeKey = `${badgeColor}-${theme}`;
const badgeClass = badgeClasses[badgeKey] ?? badgeClasses['primary-light'];
const titleClass = isLight
? 'text-4xl sm:text-5xl font-bold text-secondary-900 mb-6'
: 'text-4xl sm:text-5xl font-bold text-white mb-6';
const descClass = isLight
? 'text-xl text-secondary-600'
: 'text-xl text-secondary-300';
const containerClass = [
'text-center mx-auto mb-20',
maxWidth,
extraClass,
].filter(Boolean).join(' ');
---
<div class={containerClass}>
<span class={`inline-block px-4 py-2 rounded-full text-sm font-bold uppercase tracking-wider mb-4 ${badgeClass}`}>
{badge}
</span>
<h2 class={titleClass}>{title}</h2>
{description && (
<p class={descClass}>{description}</p>
)}
</div>
@@ -0,0 +1,63 @@
---
title: "How AI is Transforming Modern Business Operations"
description: "Explore the practical applications of artificial intelligence in streamlining business processes and driving innovation."
pubDate: 2024-01-10
heroImage: "/images/blog/ai-business.jpg"
category: "AI/ML"
tags: ["AI", "Machine Learning", "Automation", "Business"]
author:
name: "Marcus Johnson"
avatar: "/images/team/marcus.jpg"
draft: false
---
Artificial Intelligence is no longer a futuristic concept—it's reshaping how businesses operate today. From customer service chatbots to predictive analytics, AI applications are becoming essential tools for competitive advantage.
## Key AI Applications in Business
### 1. Customer Service Automation
AI-powered chatbots handle routine inquiries 24/7, freeing human agents for complex issues:
```python
from transformers import pipeline
# Simple sentiment analysis for customer feedback
classifier = pipeline("sentiment-analysis")
result = classifier("The support team was incredibly helpful!")
print(result) # [{'label': 'POSITIVE', 'score': 0.9998}]
```
### 2. Predictive Analytics
Machine learning models forecast trends, inventory needs, and customer behavior:
```python
import pandas as pd
from sklearn.ensemble import RandomForestRegressor
# Load historical sales data
df = pd.read_csv('sales_data.csv')
# Train predictive model
model = RandomForestRegressor(n_estimators=100)
model.fit(df[['month', 'marketing_spend']], df['revenue'])
# Predict next quarter
predictions = model.predict([[4, 50000]])
```
### 3. Process Automation
Robotic Process Automation (RPA) combined with AI handles repetitive tasks with minimal errors.
## Implementation Considerations
- **Data Quality** - AI is only as good as the data it learns from
- **Integration** - Ensure compatibility with existing systems
- **Ethics** - Implement responsible AI practices
- **Training** - Upskill your team to work alongside AI tools
## The Road Ahead
Companies that embrace AI strategically will see improved efficiency, reduced costs, and enhanced customer experiences. The question isn't whether to adopt AI, but how quickly you can implement it effectively.
+90
View File
@@ -0,0 +1,90 @@
---
title: "The Complete Guide to Cloud Migration in 2024"
description: "A step-by-step approach to migrating your infrastructure to the cloud while minimizing downtime and maximizing ROI."
pubDate: 2024-01-05
heroImage: "/images/blog/cloud-migration.jpg"
category: "Cloud"
tags: ["Cloud", "AWS", "Azure", "Migration", "DevOps"]
author:
name: "David Park"
avatar: "/images/team/david.jpg"
draft: false
---
Cloud migration remains one of the most impactful decisions an organization can make. This guide walks you through the essential steps for a successful transition.
## The 6 R's of Migration
Understanding your migration strategy starts with the six R's:
1. **Rehost** (Lift and Shift)
2. **Replatform** (Lift and Optimize)
3. **Repurchase** (Move to SaaS)
4. **Refactor** (Re-architect)
5. **Retire** (Decommission)
6. **Retain** (Keep On-Premises)
## Infrastructure as Code
Modern cloud deployments rely on IaC tools like Terraform:
```hcl
# Define AWS EC2 instance
resource "aws_instance" "web_server" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.medium"
tags = {
Name = "WebServer"
Environment = "Production"
}
vpc_security_group_ids = [aws_security_group.web.id]
}
# Auto-scaling configuration
resource "aws_autoscaling_group" "web" {
desired_capacity = 2
max_size = 10
min_size = 2
target_group_arns = [aws_lb_target_group.web.arn]
launch_template {
id = aws_launch_template.web.id
version = "$Latest"
}
}
```
## Cost Optimization Strategies
Cloud costs can spiral without proper governance:
```yaml
# AWS Cost allocation tags
Resources:
WebServer:
Type: AWS::EC2::Instance
Properties:
Tags:
- Key: CostCenter
Value: Engineering
- Key: Project
Value: CustomerPortal
- Key: Environment
Value: Production
```
## Migration Checklist
- [ ] Complete application inventory
- [ ] Assess dependencies and data flows
- [ ] Define success metrics
- [ ] Plan rollback procedures
- [ ] Test thoroughly in staging
- [ ] Execute migration during low-traffic windows
- [ ] Monitor performance post-migration
## Conclusion
A well-planned cloud migration delivers scalability, cost efficiency, and improved disaster recovery. Partner with experienced cloud architects to ensure your transition is smooth and successful.
@@ -0,0 +1,70 @@
---
title: "Getting Started with Astro: The Modern Static Site Generator"
description: "Learn how Astro combines the best of static site generation with modern component frameworks for blazing-fast websites."
pubDate: 2024-01-15
heroImage: "/images/blog/astro-intro.jpg"
category: "Web Development"
tags: ["Astro", "Static Sites", "JavaScript", "Performance"]
author:
name: "Sarah Chen"
avatar: "/images/team/sarah.jpg"
draft: false
---
Astro has revolutionized how we build content-focused websites. Unlike traditional frameworks, Astro ships **zero JavaScript by default**, resulting in incredibly fast page loads.
## Why Choose Astro?
Astro stands out for several compelling reasons:
1. **Island Architecture** - Only hydrate interactive components
2. **Framework Agnostic** - Use React, Vue, Svelte, or vanilla JS
3. **Content Collections** - Type-safe Markdown and MDX handling
4. **Edge-Ready** - Deploy anywhere with adapters
## Code Example
Here's a simple Astro component:
```astro
---
// Component script runs at build time
const greeting = "Hello, Astro!";
---
<h1>{greeting}</h1>
<style>
h1 {
color: #0891b2;
font-size: 2rem;
}
</style>
```
## Performance Benefits
Astro's approach to partial hydration means your users get:
- **Faster Time to Interactive (TTI)**
- **Lower JavaScript bundle sizes**
- **Better Core Web Vitals scores**
```javascript
// Traditional SPA: Everything loads
import HeavyComponent from './HeavyComponent';
import AnotherHeavyComponent from './AnotherHeavyComponent';
// Astro: Only what's needed
// Components are static HTML unless marked client:*
```
## Getting Started
Install Astro with a single command:
```bash
npm create astro@latest
```
Choose a template, and you're ready to build your next project with the performance benefits of static HTML and the developer experience of modern frameworks.
+21
View File
@@ -0,0 +1,21 @@
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: z.string().optional(),
category: z.enum(['Web Development', 'Mobile Apps', 'AI/ML', 'Cloud', 'DevOps', 'Security']),
tags: z.array(z.string()),
author: z.object({
name: z.string(),
avatar: z.string().optional(),
}),
draft: z.boolean().default(false),
}),
});
export const collections = { blog };
+1
View File
@@ -0,0 +1 @@
/// <reference path="../.astro/types.d.ts" />
+351
View File
@@ -0,0 +1,351 @@
---
// Base Layout for WorkRoot IT Solutions
// Features: SEO meta tags, global styles, header, footer slot, skip link
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
import Analytics from '../components/Analytics.astro';
import PWAInstallPrompt from '../components/PWAInstallPrompt.astro';
import '../styles/global.css';
interface Props {
title: string;
description?: string;
canonicalUrl?: string;
ogImage?: string;
noIndex?: boolean;
}
const {
title,
description = 'WorkRoot IT Solutions - Professional IT services including web development, cloud solutions, and digital transformation for businesses.',
canonicalUrl = Astro.url.href,
ogImage = '/og-image.jpg',
noIndex = false,
} = Astro.props;
const siteName = 'WorkRoot IT Solutions';
const fullTitle = title === 'Home' ? siteName : `${title} | ${siteName}`;
---
<!doctype html>
<html lang="en" class="scroll-smooth">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<!-- Primary Meta Tags -->
<title>{fullTitle}</title>
<meta name="title" content={fullTitle} />
<meta name="description" content={description} />
<meta name="author" content="WorkRoot IT Solutions" />
<meta name="keywords" content="web development, mobile app development, AI solutions, machine learning, cloud services, IT consulting, digital transformation, custom software, enterprise solutions, React, Next.js, React Native, Flutter, AWS, Azure, Python, TensorFlow" />
<meta name="language" content="English" />
<meta name="revisit-after" content="7 days" />
<meta name="rating" content="general" />
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<!-- PWA Manifest -->
<link rel="manifest" href="/manifest.json" />
<!-- Canonical URL -->
<link rel="canonical" href={canonicalUrl} />
<!-- DNS Prefetch & Preconnect for external resources -->
<link rel="dns-prefetch" href="https://images.unsplash.com" />
<link rel="preconnect" href="https://images.unsplash.com" crossorigin />
<!-- Robots -->
{noIndex ? (
<meta name="robots" content="noindex, nofollow" />
) : (
<meta name="robots" content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1" />
)}
<meta name="googlebot" content="index, follow" />
<meta name="bingbot" content="index, follow" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content={canonicalUrl} />
<meta property="og:title" content={fullTitle} />
<meta property="og:description" content={description} />
<meta property="og:image" content={new URL(ogImage, Astro.url).href} />
<meta property="og:image:secure_url" content={new URL(ogImage, Astro.url).href} />
<meta property="og:image:type" content="image/jpeg" />
<meta property="og:image:alt" content={fullTitle} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:site_name" content={siteName} />
<meta property="og:locale" content="en_US" />
<meta property="fb:app_id" content="your-facebook-app-id" />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@workroot" />
<meta name="twitter:creator" content="@workroot" />
<meta name="twitter:url" content={canonicalUrl} />
<meta name="twitter:title" content={fullTitle} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={new URL(ogImage, Astro.url).href} />
<meta name="twitter:image:alt" content={fullTitle} />
<meta name="twitter:domain" content="workroot.in" />
<meta name="twitter:label1" content="Est. reading time" />
<meta name="twitter:data1" content="3 minutes" />
<!-- Theme Color -->
<meta name="theme-color" content="#0891b2" />
<!-- JSON-LD Structured Data - Organization Schema -->
<script type="application/ld+json" set:html={JSON.stringify({
"@context": "https://schema.org",
"@type": "Organization",
"name": "WorkRoot IT Solutions",
"alternateName": "WorkRoot",
"url": "https://workroot.in",
"logo": {
"@type": "ImageObject",
"url": "https://workroot.in/logo.png",
"width": "512",
"height": "512"
},
"image": "https://workroot.in/og-image.jpg",
"description": "Professional IT services including web development, cloud solutions, and digital transformation for businesses.",
"foundingDate": "2014",
"founder": {
"@type": "Person",
"name": "Sarah Chen"
},
"address": {
"@type": "PostalAddress",
"streetAddress": "123 Tech Plaza, Suite 400",
"addressLocality": "San Francisco",
"addressRegion": "CA",
"postalCode": "94105",
"addressCountry": "US"
},
"contactPoint": [{
"@type": "ContactPoint",
"telephone": "+1-555-123-4567",
"contactType": "customer service",
"email": "hello@workroot.in",
"availableLanguage": ["English"],
"areaServed": "Worldwide"
}, {
"@type": "ContactPoint",
"telephone": "+1-555-123-4567",
"contactType": "sales",
"email": "sales@workroot.in",
"availableLanguage": ["English"],
"areaServed": "Worldwide"
}],
"sameAs": [
"https://twitter.com/workroot",
"https://linkedin.com/company/workroot",
"https://github.com/workroot",
"https://facebook.com/workroot"
],
"areaServed": {
"@type": "GeoShape",
"name": "Worldwide"
},
"serviceType": ["Web Development", "Mobile App Development", "AI & Machine Learning", "Cloud Services"],
"knowsAbout": ["Web Development", "Software Engineering", "Cloud Computing", "Artificial Intelligence", "Machine Learning", "Mobile Development"],
"slogan": "Transforming Ideas Into Digital Reality",
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.9",
"reviewCount": "127",
"bestRating": "5",
"worstRating": "1"
}
})} />
<!-- JSON-LD Structured Data - WebSite Schema -->
<script type="application/ld+json" set:html={JSON.stringify({
"@context": "https://schema.org",
"@type": "WebSite",
"name": "WorkRoot IT Solutions",
"url": "https://workroot.in",
"description": "Professional IT services including web development, cloud solutions, and digital transformation for businesses.",
"publisher": {
"@type": "Organization",
"name": "WorkRoot IT Solutions"
},
"potentialAction": {
"@type": "SearchAction",
"target": {
"@type": "EntryPoint",
"urlTemplate": "https://workroot.in/blog?search={search_term_string}"
},
"query-input": "required name=search_term_string"
},
"inLanguage": "en-US"
})} />
<!-- Preconnect to Google Fonts (must come before font stylesheet) -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<!-- Non-blocking font load: Plus Jakarta Sans (variable) + JetBrains Mono (variable) -->
<!-- Replaces the render-blocking @import that was previously in global.css -->
<link
rel="preload"
as="style"
href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap"
onload="this.onload=null;this.rel='stylesheet'"
/>
<noscript>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap" />
</noscript>
<!-- Critical CSS for above-the-fold content -->
<style is:inline>
/* Critical CSS for faster FCP - system fonts render immediately */
*,*::before,*::after{box-sizing:border-box}
html{line-height:1.5;-webkit-text-size-adjust:100%;scroll-behavior:smooth}
body{margin:0;font-family:'Plus Jakarta Sans',system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;min-height:100vh;display:flex;flex-direction:column;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}
img,video{max-width:100%;height:auto;display:block}
/* Prevent layout shift from lazy images */
img[loading="lazy"]{background:#e2e8f0}
/* Prevent CLS from images without explicit dimensions */
img{aspect-ratio:attr(width)/attr(height)}
/* Hero section above-the-fold critical styles */
.min-h-screen{min-height:100vh}
.flex{display:flex}
.flex-col{flex-direction:column}
.items-center{align-items:center}
/* Prevent animation flicker on initial load */
.animate-slide-up{animation-fill-mode:forwards}
/* Skip link - always accessible */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}
</style>
<!-- Analytics (GA4 or Plausible) -->
<Analytics />
<!-- Additional head content slot -->
<slot name="head" />
</head>
<body class="min-h-screen flex flex-col antialiased">
<!-- Skip to main content link for accessibility -->
<a
href="#main-content"
class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-[100] focus:px-4 focus:py-2 focus:bg-primary focus:text-white focus:rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-400 focus:ring-offset-2"
>
Skip to main content
</a>
<!-- Header -->
<Header />
<!-- Main Content -->
<main id="main-content" class="flex-1" tabindex="-1">
<slot />
</main>
<!-- Footer -->
<Footer />
<!-- Scripts slot for page-specific scripts -->
<slot name="scripts" />
<!-- Global external link tracking -->
<script>
import { bindExternalLinkTracking } from '../utils/analytics';
document.addEventListener('DOMContentLoaded', bindExternalLinkTracking);
</script>
<!-- Scroll-reveal & micro-interactions -->
<script>
import { initAllAnimations } from '../utils/animations';
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initAllAnimations);
} else {
initAllAnimations();
}
</script>
<!-- Button radial highlight (cursor tracker) -->
<script>
function initButtonHighlight() {
document.querySelectorAll<HTMLElement>('.btn-primary, .btn-primary-lg, .btn-accent').forEach((btn) => {
btn.addEventListener('mousemove', (e: MouseEvent) => {
const rect = btn.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 100;
const y = ((e.clientY - rect.top) / rect.height) * 100;
btn.style.setProperty('--x', `${x}%`);
btn.style.setProperty('--y', `${y}%`);
});
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initButtonHighlight);
} else {
initButtonHighlight();
}
</script>
<!-- Service Worker Registration -->
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/sw.js', { scope: '/' })
.catch(() => {
// SW registration failed silently — site still works normally
});
});
}
</script>
<!-- PWA Install Prompt -->
<PWAInstallPrompt />
</body>
</html>
<style is:global>
/* Smooth scrolling for anchor links */
html {
scroll-behavior: smooth;
}
/* Focus styles for accessibility */
:focus-visible {
outline: 2px solid theme('colors.primary.DEFAULT');
outline-offset: 2px;
}
/* Remove focus outline for mouse users */
:focus:not(:focus-visible) {
outline: none;
}
/* Selection color */
::selection {
background-color: theme('colors.primary.200');
color: theme('colors.secondary.900');
}
/* Custom scrollbar (optional - webkit only) */
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-track {
background: theme('colors.secondary.100');
}
::-webkit-scrollbar-thumb {
background: theme('colors.secondary.300');
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: theme('colors.secondary.400');
}
</style>
+180
View File
@@ -0,0 +1,180 @@
import { defineMiddleware } from 'astro:middleware';
import { logger } from './utils/logger';
import { captureException } from './utils/sentry';
/**
* Security Middleware
* Implements security headers and domain validation for workroot.in
*
* Security Headers Implemented:
* - Content-Security-Policy (CSP): Controls resource loading
* - X-Frame-Options: Prevents clickjacking
* - X-Content-Type-Options: Prevents MIME sniffing
* - Referrer-Policy: Controls referrer information
* - Permissions-Policy: Controls browser features
* - Strict-Transport-Security (HSTS): Enforces HTTPS
*/
// CSRF protection: validate Origin/Referer for state-changing API requests
function validateCsrfOrigin(request: Request, url: URL): boolean {
const method = request.method;
const path = url.pathname;
// Only enforce on state-changing API methods
if (!['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) return true;
if (!path.startsWith('/api/')) return true;
// In development, allow any origin
if (!import.meta.env.PROD) return true;
const origin = request.headers.get('origin');
const referer = request.headers.get('referer');
if (origin) {
return origin === 'https://workroot.in' || origin === 'https://www.workroot.in';
}
if (referer) {
return referer.startsWith('https://workroot.in') || referer.startsWith('https://www.workroot.in');
}
// If neither origin nor referer is present, reject (missing headers = suspicious)
return false;
}
export const onRequest = defineMiddleware(async (context, next) => {
const startTime = Date.now();
const method = context.request.method;
const path = context.url.pathname;
// CSRF origin validation for API mutations
if (!validateCsrfOrigin(context.request, context.url)) {
logger.warn('middleware', 'CSRF origin check failed', {
method,
path,
origin: context.request.headers.get('origin'),
referer: context.request.headers.get('referer'),
});
return new Response(
JSON.stringify({ success: false, error: 'Forbidden: invalid origin.' }),
{ status: 403, headers: { 'Content-Type': 'application/json' } }
);
}
let response: Response;
try {
response = await next();
} catch (err) {
const durationMs = Date.now() - startTime;
await captureException(err, {
scope: 'middleware',
extra: { method, path, durationMs },
});
// Re-throw so Astro can render its 500 page
throw err;
}
const durationMs = Date.now() - startTime;
const status = response.status;
// Log all SSR errors (5xx) and not-found (404) responses
if (status >= 500) {
logger.error('middleware', `SSR error ${status}`, { method, path, status, durationMs });
} else if (status === 404) {
logger.warn('middleware', `Not found`, { method, path, status, durationMs });
} else if (status >= 400) {
logger.warn('middleware', `Client error ${status}`, { method, path, status, durationMs });
} else {
logger.debug('middleware', `Request`, { method, path, status, durationMs });
}
// Clone the response to add headers
const modifiedResponse = new Response(response.body, response);
// Content Security Policy (CSP)
// Allows:
// - self: Same-origin resources
// - https://images.unsplash.com: External images (explicit, not wildcard https:)
// - https://fonts.googleapis.com & https://fonts.gstatic.com: Google Fonts
// - inline scripts/styles: Required for Astro framework
// - https://www.googletagmanager.com & https://www.google-analytics.com: GA4
// - https://plausible.io: Plausible Analytics
// Note: unsafe-eval is removed in production (only needed during Astro HMR dev mode)
const isProduction = import.meta.env.PROD;
const scriptSrc = isProduction
? "script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://www.google-analytics.com https://plausible.io"
: "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.googletagmanager.com https://www.google-analytics.com https://plausible.io";
const csp = [
"default-src 'self'",
scriptSrc,
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"img-src 'self' data: https://images.unsplash.com https://www.google-analytics.com https://www.googletagmanager.com",
"font-src 'self' data: https://fonts.gstatic.com",
"connect-src 'self' https://www.google-analytics.com https://analytics.google.com https://stats.g.doubleclick.net https://plausible.io",
"worker-src 'self'",
"manifest-src 'self'",
"object-src 'none'", // Block Flash/plugins (defense-in-depth)
"frame-src 'none'", // No embedded frames allowed
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
"upgrade-insecure-requests",
].join('; ');
modifiedResponse.headers.set('Content-Security-Policy', csp);
// X-Frame-Options: Prevent clickjacking
// DENY prevents any domain from framing the content
modifiedResponse.headers.set('X-Frame-Options', 'DENY');
// X-Content-Type-Options: Prevent MIME sniffing
// nosniff prevents browsers from interpreting files as different MIME type
modifiedResponse.headers.set('X-Content-Type-Options', 'nosniff');
// Referrer-Policy: Control referrer information
// strict-origin-when-cross-origin sends full URL for same-origin, origin only for cross-origin
modifiedResponse.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
// Permissions-Policy: Disable unnecessary browser features
// Restricts access to sensitive APIs to prevent abuse
modifiedResponse.headers.set('Permissions-Policy',
'geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()'
);
// Strict-Transport-Security (HSTS): Enforce HTTPS
// max-age=31536000: 1 year
// includeSubDomains: Apply to all subdomains
// preload: Allow inclusion in HSTS preload list
// Only set HSTS in production and when using HTTPS
const isHttps = context.url.protocol === 'https:';
if (isProduction && isHttps) {
modifiedResponse.headers.set(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload'
);
}
// X-Permitted-Cross-Domain-Policies: Restrict cross-domain access
modifiedResponse.headers.set('X-Permitted-Cross-Domain-Policies', 'none');
// X-DNS-Prefetch-Control: Control DNS prefetching
modifiedResponse.headers.set('X-DNS-Prefetch-Control', 'on');
// Domain validation: Ensure requests are for workroot.in
const allowedHosts = ['workroot.in', 'www.workroot.in', 'localhost', '127.0.0.1', '0.0.0.0'];
const requestHost = context.url.hostname;
// In development, allow any host
if (!isProduction || allowedHosts.includes(requestHost)) {
return modifiedResponse;
}
// In production, if host doesn't match, redirect to canonical domain
if (isProduction && !allowedHosts.includes(requestHost)) {
const canonicalUrl = `https://workroot.in${context.url.pathname}${context.url.search}`;
return Response.redirect(canonicalUrl, 301);
}
return modifiedResponse;
});
+490
View File
@@ -0,0 +1,490 @@
---
// About Us Page for WorkRoot IT Solutions
// Features: Company story, team member cards, mission/vision statements, company timeline
import BaseLayout from '../layouts/BaseLayout.astro';
import SEO from '../components/SEO.astro';
// Team member data
const teamMembers = [
{
name: 'Sarah Chen',
role: 'CEO & Founder',
bio: 'Visionary leader with 15+ years in enterprise technology. Former VP at Fortune 500 tech company.',
image: 'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=400&h=400&fit=crop&crop=face&auto=format&q=75',
linkedin: '#',
twitter: '#',
},
{
name: 'Michael Rodriguez',
role: 'CTO',
bio: 'Cloud architecture expert and open-source contributor. Passionate about scalable solutions.',
image: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=400&h=400&fit=crop&crop=face&auto=format&q=75',
linkedin: '#',
twitter: '#',
},
{
name: 'Emily Watson',
role: 'Head of Design',
bio: 'Award-winning UX designer focused on creating intuitive, accessible digital experiences.',
image: 'https://images.unsplash.com/photo-1580489944761-15a19d654956?w=400&h=400&fit=crop&crop=face&auto=format&q=75',
linkedin: '#',
twitter: '#',
},
{
name: 'David Park',
role: 'Lead Developer',
bio: 'Full-stack engineer specializing in React and Node.js. Mentors junior developers.',
image: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=400&fit=crop&crop=face&auto=format&q=75',
linkedin: '#',
twitter: '#',
},
{
name: 'Lisa Thompson',
role: 'Project Manager',
bio: 'PMP-certified professional delivering projects on time and within budget for 10+ years.',
image: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=400&h=400&fit=crop&crop=face&auto=format&q=75',
linkedin: '#',
twitter: '#',
},
{
name: 'James Wilson',
role: 'DevOps Engineer',
bio: 'Infrastructure automation specialist. Kubernetes and AWS certified solutions architect.',
image: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=400&h=400&fit=crop&crop=face&auto=format&q=75',
linkedin: '#',
twitter: '#',
},
];
// Company stats
const stats = [
{ value: '10+', label: 'Years of Experience' },
{ value: '200+', label: 'Projects Delivered' },
{ value: '50+', label: 'Team Members' },
{ value: '98%', label: 'Client Satisfaction' },
];
// Company timeline
const timeline = [
{
year: '2014',
title: 'The Beginning',
description: 'WorkRoot founded by three passionate developers in a small garage office.',
},
{
year: '2016',
title: 'First Major Client',
description: 'Secured our first enterprise contract, launching our growth trajectory.',
},
{
year: '2018',
title: 'Team Expansion',
description: 'Grew to 20+ team members and opened our first dedicated office space.',
},
{
year: '2020',
title: 'Global Reach',
description: 'Expanded operations internationally, serving clients across 15 countries.',
},
{
year: '2022',
title: 'AI Innovation',
description: 'Launched AI/ML division, pioneering intelligent automation solutions.',
},
{
year: '2024',
title: 'Industry Leader',
description: 'Recognized as top IT solutions provider with 200+ successful projects.',
},
];
// Core values
const values = [
{
title: 'Innovation',
description: 'We embrace cutting-edge technologies to deliver future-proof solutions.',
icon: 'lightbulb',
},
{
title: 'Integrity',
description: 'Transparency and honesty are the foundation of every client relationship.',
icon: 'shield',
},
{
title: 'Excellence',
description: 'We pursue quality in every line of code and every interaction.',
icon: 'star',
},
{
title: 'Collaboration',
description: 'Great solutions emerge from diverse perspectives working together.',
icon: 'users',
},
];
---
<BaseLayout
title="About Us — Our Story & Team"
description="WorkRoot IT Solutions: founded 2014, 50+ team members, 200+ projects, 15 countries served. Meet the technologists behind your digital transformation. Innovation, integrity, excellence."
>
<SEO
slot="head"
type="AboutPage"
breadcrumbs={[
{ name: 'Home', url: '/' },
{ name: 'About Us', url: '/about' }
]}
/>
<!-- Hero Section -->
<section class="relative min-h-[70vh] flex items-center overflow-hidden bg-gradient-to-br from-secondary-900 via-secondary-800 to-primary-900">
<!-- Background decorative elements -->
<div class="absolute inset-0 overflow-hidden pointer-events-none" aria-hidden="true">
<div class="absolute -top-40 -right-40 w-96 h-96 bg-primary/20 rounded-full blur-3xl"></div>
<div class="absolute -bottom-40 -left-40 w-96 h-96 bg-accent/10 rounded-full blur-3xl"></div>
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[800px] bg-primary/5 rounded-full blur-3xl"></div>
<!-- Grid pattern overlay -->
<div class="absolute inset-0 bg-[linear-gradient(rgba(255,255,255,0.03)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.03)_1px,transparent_1px)] bg-[size:64px_64px]"></div>
</div>
<div class="container-wrapper relative z-10 py-20">
<div class="max-w-4xl">
<span class="inline-block px-4 py-1.5 mb-6 text-sm font-medium text-primary-300 bg-primary-500/10 rounded-full border border-primary-500/20">
About WorkRoot
</span>
<h1 class="text-4xl sm:text-5xl lg:text-6xl xl:text-7xl font-bold text-white leading-tight mb-6">
Building the Future of
<span class="block text-transparent bg-clip-text bg-gradient-to-r from-primary-300 to-accent-400 mt-2">
Digital Innovation
</span>
</h1>
<p class="text-lg sm:text-xl text-secondary-200 max-w-2xl mb-8">
We're a team of passionate technologists dedicated to transforming businesses through innovative IT solutions.
</p>
<!-- Quick stats in hero -->
<div class="grid grid-cols-2 sm:grid-cols-4 gap-6 mt-10 max-w-3xl">
{stats.map((stat) => (
<div class="text-center sm:text-left">
<p class="text-3xl sm:text-4xl font-bold text-primary-300 mb-1">{stat.value}</p>
<p class="text-secondary-300 text-sm font-medium">{stat.label}</p>
</div>
))}
</div>
</div>
</div>
</section>
<!-- Company Story Section -->
<section class="py-16 lg:py-24 bg-white">
<div class="container-wrapper">
<div class="grid lg:grid-cols-2 gap-12 lg:gap-16 items-center">
<div class="order-2 lg:order-1" data-animate="fade-right">
<span class="text-primary-500 font-semibold text-sm uppercase tracking-wider">Our Story</span>
<h2 class="text-3xl sm:text-4xl font-bold text-secondary-900 mt-2 mb-6">
From Startup to Industry Leader
</h2>
<div class="space-y-4 text-secondary-600 leading-relaxed">
<p>
Founded in 2014, WorkRoot IT Solutions began with a simple mission: to make enterprise-grade technology accessible to businesses of all sizes. What started as a small team of three passionate developers has grown into a full-service IT consultancy trusted by companies worldwide.
</p>
<p>
Our journey hasn't always been easy. We've navigated economic downturns, technological shifts, and global challenges. But each obstacle has strengthened our resolve and sharpened our expertise. Today, we're proud to serve over 200 clients across diverse industries.
</p>
<p>
What sets us apart isn't just our technical skills—it's our commitment to understanding your business. We don't believe in one-size-fits-all solutions. Every project we undertake is tailored to your unique needs, goals, and constraints.
</p>
</div>
</div>
<div class="order-1 lg:order-2 relative" data-animate="fade-left" data-delay="150">
<div class="aspect-[4/3] rounded-2xl overflow-hidden shadow-2xl bg-secondary-200">
<img
src="https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=800&h=600&fit=crop&auto=format&q=75"
alt="WorkRoot team collaborating in office"
width="800"
height="600"
class="w-full h-full object-cover"
loading="eager"
decoding="async"
fetchpriority="high"
/>
</div>
<div class="absolute -bottom-6 -left-6 bg-primary-500 text-white p-6 rounded-xl shadow-xl hidden sm:block">
<p class="text-4xl font-bold">10+</p>
<p class="text-primary-100">Years of Excellence</p>
</div>
</div>
</div>
</div>
</section>
<!-- Company Timeline Section -->
<section class="py-16 lg:py-24 bg-white">
<div class="container-wrapper">
<div class="text-center max-w-2xl mx-auto mb-12 lg:mb-16" data-animate="fade-up">
<span class="text-primary-500 font-semibold text-sm uppercase tracking-wider">Our Journey</span>
<h2 class="text-3xl sm:text-4xl font-bold text-secondary-900 mt-2 mb-4">Company Timeline</h2>
<p class="text-secondary-600">
A decade of innovation, growth, and excellence in technology solutions.
</p>
</div>
<div class="max-w-5xl mx-auto">
<!-- Timeline container -->
<div class="relative">
<!-- Vertical line (hidden on mobile, visible on md+) -->
<div class="hidden md:block absolute left-1/2 top-0 bottom-0 w-0.5 bg-gradient-to-b from-primary-500 via-primary-300 to-primary-500 transform -translate-x-1/2" aria-hidden="true"></div>
<div class="space-y-8 md:space-y-12">
{timeline.map((item, index) => (
<div class={`relative ${index % 2 === 0 ? 'md:mr-auto md:pr-12' : 'md:ml-auto md:pl-12'} md:w-1/2`} data-animate={index % 2 === 0 ? 'fade-right' : 'fade-left'} data-delay={String(index * 100)}>
<!-- Timeline dot -->
<div class="hidden md:flex absolute top-0 w-4 h-4 bg-primary-500 rounded-full border-4 border-white shadow-lg z-10"
style={index % 2 === 0 ? 'right: -2.25rem;' : 'left: -2.25rem;'}
aria-hidden="true">
</div>
<!-- Content card -->
<div class={`relative bg-white border-2 border-secondary-100 rounded-2xl p-6 shadow-sm hover:shadow-lg transition-all duration-300 group hover:border-primary-200 ${index % 2 === 0 ? 'md:text-right' : 'md:text-left'}`}>
<div class="absolute -top-3 left-6 md:left-auto md:right-6 px-4 py-1 bg-gradient-to-r from-primary-500 to-primary-600 text-white text-sm font-bold rounded-full shadow-md">
{item.year}
</div>
<h3 class="text-xl font-bold text-secondary-900 mt-4 mb-2 group-hover:text-primary-600 transition-colors">
{item.title}
</h3>
<p class="text-secondary-600 leading-relaxed">
{item.description}
</p>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</section>
<!-- Mission & Vision Section -->
<section class="py-16 lg:py-24 bg-gradient-to-b from-secondary-50 to-white">
<div class="container-wrapper">
<div class="text-center mb-12 lg:mb-16" data-animate="fade-up">
<span class="text-primary-500 font-semibold text-sm uppercase tracking-wider">Our Purpose</span>
<h2 class="text-3xl sm:text-4xl font-bold text-secondary-900 mt-2">Mission & Vision</h2>
</div>
<div class="grid lg:grid-cols-2 gap-8 lg:gap-12">
<!-- Mission -->
<div class="relative group" data-animate="fade-right" data-delay="100">
<div class="absolute inset-0 bg-gradient-to-br from-primary-500 to-primary-700 rounded-3xl transform transition-transform group-hover:scale-105"></div>
<div class="relative p-8 lg:p-12 bg-gradient-to-br from-primary-500 to-primary-700 rounded-3xl text-white overflow-hidden">
<div class="absolute top-0 right-0 w-64 h-64 bg-white/10 rounded-full -translate-y-1/2 translate-x-1/2 blur-2xl"></div>
<div class="relative">
<div class="w-16 h-16 bg-white/20 backdrop-blur-sm rounded-2xl flex items-center justify-center mb-6 shadow-lg">
<svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<h3 class="text-2xl lg:text-3xl font-bold mb-4">Our Mission</h3>
<p class="text-primary-50 leading-relaxed text-lg">
To empower businesses through innovative technology solutions that drive growth, enhance efficiency, and create lasting value. We bridge the gap between complex technology and practical business outcomes.
</p>
</div>
</div>
</div>
<!-- Vision -->
<div class="relative group" data-animate="fade-left" data-delay="250">
<div class="absolute inset-0 bg-gradient-to-br from-secondary-800 to-secondary-900 rounded-3xl transform transition-transform group-hover:scale-105"></div>
<div class="relative p-8 lg:p-12 bg-gradient-to-br from-secondary-800 to-secondary-900 rounded-3xl text-white overflow-hidden">
<div class="absolute top-0 right-0 w-64 h-64 bg-primary-500/10 rounded-full -translate-y-1/2 translate-x-1/2 blur-2xl"></div>
<div class="relative">
<div class="w-16 h-16 bg-white/10 backdrop-blur-sm rounded-2xl flex items-center justify-center mb-6 shadow-lg">
<svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" 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" />
</svg>
</div>
<h3 class="text-2xl lg:text-3xl font-bold mb-4">Our Vision</h3>
<p class="text-secondary-200 leading-relaxed text-lg">
To be the most trusted technology partner for businesses worldwide, known for our integrity, innovation, and unwavering commitment to client success. We envision a future where every business can harness the full potential of technology.
</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Core Values Section -->
<section class="py-16 lg:py-24 bg-white relative overflow-hidden">
<!-- Background decoration -->
<div class="absolute inset-0 overflow-hidden pointer-events-none" aria-hidden="true">
<div class="absolute top-0 right-0 w-96 h-96 bg-primary-500/5 rounded-full blur-3xl"></div>
<div class="absolute bottom-0 left-0 w-96 h-96 bg-accent-500/5 rounded-full blur-3xl"></div>
</div>
<div class="container-wrapper relative">
<div class="text-center max-w-2xl mx-auto mb-12 lg:mb-16" data-animate="fade-up">
<span class="text-primary-500 font-semibold text-sm uppercase tracking-wider">What Drives Us</span>
<h2 class="text-3xl sm:text-4xl font-bold text-secondary-900 mt-2 mb-4">Our Core Values</h2>
<p class="text-secondary-600">
These principles guide every decision we make and every solution we deliver.
</p>
</div>
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8">
{values.map((value, index) => (
<div class="group relative" data-animate="fade-up" data-delay={String(index * 100)}>
<!-- Gradient background on hover -->
<div class="absolute inset-0 bg-gradient-to-br from-primary-500 to-primary-700 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<!-- Card content -->
<div class="relative bg-white p-6 lg:p-8 rounded-2xl shadow-sm group-hover:shadow-xl transition-all duration-300 border-2 border-secondary-100 group-hover:border-primary-200 group-hover:translate-y-[-4px]">
<div class="w-14 h-14 bg-gradient-to-br from-primary-50 to-primary-100 text-primary-600 rounded-xl flex items-center justify-center mb-6 group-hover:bg-white group-hover:text-white transition-all duration-300 shadow-md">
{value.icon === 'lightbulb' && (
<svg xmlns="http://www.w3.org/2000/svg" class="w-7 h-7" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
)}
{value.icon === 'shield' && (
<svg xmlns="http://www.w3.org/2000/svg" class="w-7 h-7" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" 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>
)}
{value.icon === 'star' && (
<svg xmlns="http://www.w3.org/2000/svg" class="w-7 h-7" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
</svg>
)}
{value.icon === 'users' && (
<svg xmlns="http://www.w3.org/2000/svg" class="w-7 h-7" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
)}
</div>
<h3 class="text-xl font-bold text-secondary-900 mb-3 group-hover:text-white transition-colors duration-300">{value.title}</h3>
<p class="text-secondary-600 leading-relaxed group-hover:text-primary-50 transition-colors duration-300">{value.description}</p>
</div>
</div>
))}
</div>
</div>
</section>
<!-- Team Section -->
<section class="py-16 lg:py-24 bg-secondary-50">
<div class="container-wrapper">
<div class="text-center max-w-2xl mx-auto mb-12 lg:mb-16" data-animate="fade-up">
<span class="text-primary-500 font-semibold text-sm uppercase tracking-wider">The Team</span>
<h2 class="text-3xl sm:text-4xl font-bold text-secondary-900 mt-2 mb-4">Meet Our Leadership</h2>
<p class="text-secondary-600">
Talented professionals who bring diverse expertise and shared passion for technology excellence.
</p>
</div>
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-8 lg:gap-10">
{teamMembers.map((member, index) => (
<article class="group relative" data-animate="fade-up" data-delay={String((index % 3) * 100)}>
<!-- Card wrapper with gradient border effect -->
<div class="absolute inset-0 bg-gradient-to-br from-primary-400 to-accent-400 rounded-3xl opacity-0 group-hover:opacity-100 transition-opacity duration-300 blur-sm"></div>
<div class="relative bg-white rounded-3xl overflow-hidden shadow-md hover:shadow-2xl transition-all duration-300 transform group-hover:-translate-y-2">
<!-- Image container -->
<div class="relative aspect-[4/5] overflow-hidden bg-secondary-200">
<img
src={member.image}
alt={`${member.name} - ${member.role}`}
width="400"
height="500"
class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700"
loading={index === 0 ? 'eager' : 'lazy'}
fetchpriority={index === 0 ? 'high' : 'auto'}
decoding="async"
/>
<!-- Gradient overlay -->
<div class="absolute inset-0 bg-gradient-to-t from-secondary-900/90 via-secondary-900/30 to-transparent opacity-60 group-hover:opacity-80 transition-opacity duration-300"></div>
<!-- Social links overlay -->
<div class="absolute inset-x-0 bottom-0 p-6 transform translate-y-full group-hover:translate-y-0 transition-transform duration-300">
<div class="flex justify-center gap-3">
<a
href={member.linkedin}
class="w-11 h-11 bg-white/95 hover:bg-white rounded-xl flex items-center justify-center text-secondary-700 hover:text-primary-500 transition-all shadow-lg hover:scale-110"
aria-label={`${member.name}'s LinkedIn profile`}
>
<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>
</a>
<a
href={member.twitter}
class="w-11 h-11 bg-white/95 hover:bg-white rounded-xl flex items-center justify-center text-secondary-700 hover:text-primary-500 transition-all shadow-lg hover:scale-110"
aria-label={`${member.name}'s Twitter profile`}
>
<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>
</a>
</div>
</div>
</div>
<!-- Content -->
<div class="p-6 lg:p-7">
<h3 class="text-xl font-bold text-secondary-900 mb-1 group-hover:text-primary-600 transition-colors">{member.name}</h3>
<p class="text-primary-500 font-semibold text-sm mb-3 uppercase tracking-wide">{member.role}</p>
<p class="text-secondary-600 leading-relaxed text-sm">{member.bio}</p>
</div>
</div>
</article>
))}
</div>
</div>
</section>
<!-- CTA Section -->
<section class="relative py-20 lg:py-28 overflow-hidden">
<!-- Gradient background -->
<div class="absolute inset-0 bg-gradient-to-br from-primary-600 via-primary-700 to-secondary-800"></div>
<!-- Decorative elements -->
<div class="absolute inset-0 overflow-hidden pointer-events-none" aria-hidden="true">
<div class="absolute -top-40 -right-40 w-96 h-96 bg-accent-500/20 rounded-full blur-3xl"></div>
<div class="absolute -bottom-40 -left-40 w-96 h-96 bg-primary-400/20 rounded-full blur-3xl"></div>
<div class="absolute inset-0 bg-[linear-gradient(rgba(255,255,255,0.05)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.05)_1px,transparent_1px)] bg-[size:64px_64px]"></div>
</div>
<div class="container-wrapper relative z-10">
<div class="max-w-4xl mx-auto text-center">
<div class="inline-block px-4 py-1.5 mb-6 text-sm font-medium text-primary-200 bg-white/10 rounded-full border border-white/20 backdrop-blur-sm">
Let's Build Together
</div>
<h2 class="text-3xl sm:text-4xl lg:text-5xl font-bold text-white mb-6">
Ready to Work With Us?
</h2>
<p class="text-primary-50 text-lg sm:text-xl max-w-2xl mx-auto mb-10">
Let's discuss how we can help transform your business with innovative technology solutions.
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a
href="/contact"
class="group inline-flex items-center justify-center px-8 py-4 bg-white text-primary-600 font-semibold rounded-xl hover:bg-primary-50 transition-all shadow-xl hover:shadow-2xl hover:scale-105 focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-primary-600"
>
Get in Touch
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 ml-2 group-hover:translate-x-1 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</a>
<a
href="/services"
class="inline-flex items-center justify-center px-8 py-4 border-2 border-white/40 text-white font-semibold rounded-xl hover:bg-white/10 hover:border-white/60 transition-all backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-primary-600"
>
View Our Services
</a>
</div>
</div>
</div>
</section>
</BaseLayout>
+326
View File
@@ -0,0 +1,326 @@
import type { APIRoute } from 'astro';
import { logger, logApiRequest } from '../../utils/logger';
import { captureException } from '../../utils/sentry';
// ============================================================
// In-memory rate limiter (sliding window, per IP)
// ============================================================
interface RateLimitEntry {
timestamps: number[];
}
const rateLimitStore = new Map<string, RateLimitEntry>();
const RATE_LIMIT_WINDOW_MS = 60 * 60 * 1000; // 1 hour
const RATE_LIMIT_MAX_REQUESTS = 5; // max 5 contact submissions per hour per IP
function checkRateLimit(ip: string): { allowed: boolean; remaining: number; resetAt: number } {
const now = Date.now();
const windowStart = now - RATE_LIMIT_WINDOW_MS;
const entry = rateLimitStore.get(ip) ?? { timestamps: [] };
// Remove timestamps outside the window
entry.timestamps = entry.timestamps.filter((ts) => ts > windowStart);
const remaining = RATE_LIMIT_MAX_REQUESTS - entry.timestamps.length;
const resetAt = entry.timestamps.length > 0
? entry.timestamps[0] + RATE_LIMIT_WINDOW_MS
: now + RATE_LIMIT_WINDOW_MS;
if (entry.timestamps.length >= RATE_LIMIT_MAX_REQUESTS) {
rateLimitStore.set(ip, entry);
return { allowed: false, remaining: 0, resetAt };
}
entry.timestamps.push(now);
rateLimitStore.set(ip, entry);
return { allowed: true, remaining: remaining - 1, resetAt };
}
// ============================================================
// Input validation
// ============================================================
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const PHONE_REGEX = /^[\+]?[(]?[0-9]{1,3}[)]?[-\s\.]?[(]?[0-9]{1,4}[)]?[-\s\.]?[0-9]{1,4}[-\s\.]?[0-9]{1,9}$/;
const VALID_SUBJECTS = [
'web-development',
'mobile-development',
'cloud-services',
'ai-ml',
'consulting',
'support',
'other',
];
interface ContactFormData {
name: string;
email: string;
phone?: string;
subject: string;
message: string;
website?: string; // honeypot
}
function validateContactForm(data: ContactFormData): string | null {
if (!data.name || data.name.trim().length < 2) {
return 'Name must be at least 2 characters.';
}
if (data.name.trim().length > 100) {
return 'Name must be under 100 characters.';
}
if (!data.email || !EMAIL_REGEX.test(data.email.trim())) {
return 'A valid email address is required.';
}
if (data.email.trim().length > 254) {
return 'Email address is too long.';
}
if (data.phone && data.phone.trim() !== '' && !PHONE_REGEX.test(data.phone.trim())) {
return 'Phone number format is invalid.';
}
if (!data.subject || !VALID_SUBJECTS.includes(data.subject)) {
return 'Please select a valid subject.';
}
if (!data.message || data.message.trim().length < 10) {
return 'Message must be at least 10 characters.';
}
if (data.message.trim().length > 5000) {
return 'Message must be under 5000 characters.';
}
return null;
}
// ============================================================
// Email sending via SMTP (nodemailer) or no-op in development
// ============================================================
async function sendContactEmail(data: ContactFormData): Promise<void> {
const smtpHost = import.meta.env.SMTP_HOST;
const smtpUser = import.meta.env.SMTP_USER;
const smtpPass = import.meta.env.SMTP_PASS;
const smtpPort = parseInt(import.meta.env.SMTP_PORT ?? '587', 10);
const recipientEmail = import.meta.env.CONTACT_EMAIL ?? import.meta.env.SMTP_USER;
if (!smtpHost || !smtpUser || !smtpPass || !recipientEmail) {
// SMTP not configured - log and continue (graceful degradation)
logger.info('api.contact', 'SMTP not configured — form submission logged only', {
email: data.email,
subject: data.subject,
name: data.name,
});
return;
}
// Dynamic import to avoid bundling issues when nodemailer isn't installed
try {
const nodemailer = await import('nodemailer');
const transporter = nodemailer.default.createTransport({
host: smtpHost,
port: smtpPort,
secure: smtpPort === 465,
auth: { user: smtpUser, pass: smtpPass },
});
const subjectLabels: Record<string, string> = {
'web-development': 'Web Development',
'mobile-development': 'Mobile Development',
'cloud-services': 'Cloud Services',
'ai-ml': 'AI & Machine Learning',
'consulting': 'IT Consulting',
'support': 'Technical Support',
'other': 'Other',
};
await transporter.sendMail({
from: `"WorkRoot Contact Form" <${smtpUser}>`,
to: recipientEmail,
replyTo: data.email,
subject: `[Contact Form] ${subjectLabels[data.subject] ?? data.subject} - ${data.name}`,
text: [
`New contact form submission`,
``,
`Name: ${data.name}`,
`Email: ${data.email}`,
`Phone: ${data.phone || 'Not provided'}`,
`Subject: ${subjectLabels[data.subject] ?? data.subject}`,
``,
`Message:`,
data.message,
``,
`---`,
`Submitted at: ${new Date().toISOString()}`,
].join('\n'),
html: `
<h2>New Contact Form Submission</h2>
<table style="border-collapse:collapse;width:100%;max-width:600px">
<tr><td style="padding:8px;font-weight:bold;border-bottom:1px solid #eee">Name</td><td style="padding:8px;border-bottom:1px solid #eee">${escapeHtml(data.name)}</td></tr>
<tr><td style="padding:8px;font-weight:bold;border-bottom:1px solid #eee">Email</td><td style="padding:8px;border-bottom:1px solid #eee"><a href="mailto:${escapeHtml(data.email)}">${escapeHtml(data.email)}</a></td></tr>
<tr><td style="padding:8px;font-weight:bold;border-bottom:1px solid #eee">Phone</td><td style="padding:8px;border-bottom:1px solid #eee">${escapeHtml(data.phone || 'Not provided')}</td></tr>
<tr><td style="padding:8px;font-weight:bold;border-bottom:1px solid #eee">Subject</td><td style="padding:8px;border-bottom:1px solid #eee">${escapeHtml(subjectLabels[data.subject] ?? data.subject)}</td></tr>
<tr><td style="padding:8px;font-weight:bold;vertical-align:top">Message</td><td style="padding:8px;white-space:pre-wrap">${escapeHtml(data.message)}</td></tr>
</table>
<p style="color:#888;font-size:12px;margin-top:16px">Submitted at: ${new Date().toISOString()}</p>
`,
});
} catch (err) {
// If nodemailer isn't installed, treat as graceful degradation (log only)
if (err instanceof Error && err.message.includes('Cannot find module')) {
logger.warn('api.contact', 'nodemailer not available — form submission logged only', {
email: data.email,
subject: data.subject,
});
return;
}
await captureException(err, {
scope: 'api.contact',
extra: { email: data.email, subject: data.subject },
});
throw new Error('Email delivery failed');
}
}
function escapeHtml(str: string): string {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
// ============================================================
// CORS headers helper
// ============================================================
function corsHeaders(): HeadersInit {
const origin = import.meta.env.PROD ? 'https://workroot.in' : '*';
return {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': origin,
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};
}
// ============================================================
// OPTIONS preflight handler
// ============================================================
export const OPTIONS: APIRoute = async () => {
return new Response(null, { status: 204, headers: corsHeaders() });
};
// ============================================================
// POST handler
// ============================================================
export const POST: APIRoute = async ({ request, clientAddress }) => {
const ip = clientAddress ?? 'unknown';
const startTime = Date.now();
// Rate limiting check
const rateLimit = checkRateLimit(ip);
const rateLimitHeaders = {
...corsHeaders(),
'X-RateLimit-Limit': String(RATE_LIMIT_MAX_REQUESTS),
'X-RateLimit-Remaining': String(rateLimit.remaining),
'X-RateLimit-Reset': String(Math.ceil(rateLimit.resetAt / 1000)),
};
if (!rateLimit.allowed) {
logApiRequest({ scope: 'api.contact', method: 'POST', path: '/api/contact', status: 429, ip });
return new Response(
JSON.stringify({
success: false,
error: 'Too many requests. Please try again later.',
}),
{ status: 429, headers: rateLimitHeaders }
);
}
// Reject oversized payloads (max 16KB)
const contentLength = parseInt(request.headers.get('content-length') ?? '0', 10);
if (contentLength > 16384) {
return new Response(
JSON.stringify({ success: false, error: 'Request body too large.' }),
{ status: 413, headers: rateLimitHeaders }
);
}
// Parse request body
let body: Record<string, unknown>;
try {
const contentType = request.headers.get('content-type') ?? '';
if (contentType.includes('application/json')) {
body = await request.json();
} else {
const formData = await request.formData();
body = Object.fromEntries(formData.entries());
}
} catch {
return new Response(
JSON.stringify({ success: false, error: 'Invalid request body.' }),
{ status: 400, headers: rateLimitHeaders }
);
}
// Honeypot check (spam bot detection)
// Bots typically fill all fields; the honeypot field should always be empty
if (body.website && String(body.website).trim() !== '') {
// Silently succeed to not reveal spam detection
return new Response(
JSON.stringify({ success: true, message: 'Message received. Thank you!' }),
{ status: 200, headers: rateLimitHeaders }
);
}
// Build and validate form data
const formData: ContactFormData = {
name: String(body.name ?? '').trim(),
email: String(body.email ?? '').trim(),
phone: body.phone ? String(body.phone).trim() : undefined,
subject: String(body.subject ?? '').trim(),
message: String(body.message ?? '').trim(),
};
const validationError = validateContactForm(formData);
if (validationError) {
return new Response(
JSON.stringify({ success: false, error: validationError }),
{ status: 422, headers: rateLimitHeaders }
);
}
// Send email
try {
await sendContactEmail(formData);
} catch {
logApiRequest({
scope: 'api.contact',
method: 'POST',
path: '/api/contact',
status: 500,
ip,
durationMs: Date.now() - startTime,
});
return new Response(
JSON.stringify({
success: false,
error: 'Failed to send message. Please try again or email us directly.',
}),
{ status: 500, headers: rateLimitHeaders }
);
}
logApiRequest({
scope: 'api.contact',
method: 'POST',
path: '/api/contact',
status: 200,
ip,
durationMs: Date.now() - startTime,
});
return new Response(
JSON.stringify({
success: true,
message: "Message sent successfully! We'll get back to you within 24 hours.",
}),
{ status: 200, headers: rateLimitHeaders }
);
};
+37
View File
@@ -0,0 +1,37 @@
import type { APIRoute } from 'astro';
const startTime = Date.now();
export const GET: APIRoute = async () => {
const uptimeSeconds = Math.floor((Date.now() - startTime) / 1000);
const memUsage = process.memoryUsage();
const payload = {
status: 'ok',
timestamp: new Date().toISOString(),
uptime: uptimeSeconds,
version: import.meta.env.npm_package_version ?? '1.0.0',
mode: 'ssr',
adapter: 'node-standalone',
domain: 'workroot.in',
memory: {
heapUsedMB: Math.round(memUsage.heapUsed / 1024 / 1024),
heapTotalMB: Math.round(memUsage.heapTotal / 1024 / 1024),
rssMB: Math.round(memUsage.rss / 1024 / 1024),
},
checks: {
server: 'ok',
},
};
return new Response(JSON.stringify(payload), {
status: 200,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Access-Control-Allow-Origin': 'https://workroot.in',
'Access-Control-Allow-Methods': 'GET',
'Access-Control-Allow-Headers': 'Content-Type',
},
});
};
+96
View File
@@ -0,0 +1,96 @@
import type { APIRoute } from 'astro';
// Module-level counters (persist across requests within same process)
let requestCount = 0;
let errorCount = 0;
const processStartTime = Date.now();
// Simple response time tracking (last 100 samples)
const responseTimes: number[] = [];
const MAX_SAMPLES = 100;
export function recordRequest(durationMs: number, isError = false) {
requestCount++;
if (isError) errorCount++;
responseTimes.push(durationMs);
if (responseTimes.length > MAX_SAMPLES) responseTimes.shift();
}
function percentile(sorted: number[], p: number): number {
if (sorted.length === 0) return 0;
const idx = Math.ceil((p / 100) * sorted.length) - 1;
return sorted[Math.max(0, idx)];
}
export const GET: APIRoute = async ({ request }) => {
// Restrict metrics to internal/authorized access only
const authHeader = request.headers.get('Authorization');
const metricsToken = import.meta.env.METRICS_TOKEN;
if (metricsToken && authHeader !== `Bearer ${metricsToken}`) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
status: 401,
headers: { 'Content-Type': 'application/json' },
});
}
const memUsage = process.memoryUsage();
const uptimeSeconds = Math.floor((Date.now() - processStartTime) / 1000);
const sorted = [...responseTimes].sort((a, b) => a - b);
const avgResponseTime = sorted.length > 0
? Math.round(sorted.reduce((a, b) => a + b, 0) / sorted.length)
: 0;
const metrics = {
timestamp: new Date().toISOString(),
uptime: {
seconds: uptimeSeconds,
human: formatUptime(uptimeSeconds),
},
requests: {
total: requestCount,
errors: errorCount,
errorRate: requestCount > 0 ? ((errorCount / requestCount) * 100).toFixed(2) + '%' : '0%',
},
responseTime: {
avgMs: avgResponseTime,
p50Ms: percentile(sorted, 50),
p95Ms: percentile(sorted, 95),
p99Ms: percentile(sorted, 99),
samples: sorted.length,
},
memory: {
heapUsedMB: Math.round(memUsage.heapUsed / 1024 / 1024),
heapTotalMB: Math.round(memUsage.heapTotal / 1024 / 1024),
externalMB: Math.round(memUsage.external / 1024 / 1024),
rssMB: Math.round(memUsage.rss / 1024 / 1024),
},
process: {
pid: process.pid,
nodeVersion: process.version,
platform: process.platform,
},
};
return new Response(JSON.stringify(metrics, null, 2), {
status: 200,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-cache, no-store, must-revalidate',
},
});
};
function formatUptime(seconds: number): string {
const d = Math.floor(seconds / 86400);
const h = Math.floor((seconds % 86400) / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = seconds % 60;
const parts = [];
if (d > 0) parts.push(`${d}d`);
if (h > 0) parts.push(`${h}h`);
if (m > 0) parts.push(`${m}m`);
parts.push(`${s}s`);
return parts.join(' ');
}
+273
View File
@@ -0,0 +1,273 @@
import type { APIRoute } from 'astro';
import { logger, logApiRequest } from '../../utils/logger';
import { captureException } from '../../utils/sentry';
// ============================================================
// In-memory rate limiter (sliding window, per IP)
// ============================================================
interface RateLimitEntry {
timestamps: number[];
}
const rateLimitStore = new Map<string, RateLimitEntry>();
const RATE_LIMIT_WINDOW_MS = 60 * 60 * 1000; // 1 hour
const RATE_LIMIT_MAX_REQUESTS = 3; // max 3 newsletter signups per hour per IP
function checkRateLimit(ip: string): { allowed: boolean; remaining: number; resetAt: number } {
const now = Date.now();
const windowStart = now - RATE_LIMIT_WINDOW_MS;
const entry = rateLimitStore.get(ip) ?? { timestamps: [] };
entry.timestamps = entry.timestamps.filter((ts) => ts > windowStart);
const remaining = RATE_LIMIT_MAX_REQUESTS - entry.timestamps.length;
const resetAt = entry.timestamps.length > 0
? entry.timestamps[0] + RATE_LIMIT_WINDOW_MS
: now + RATE_LIMIT_WINDOW_MS;
if (entry.timestamps.length >= RATE_LIMIT_MAX_REQUESTS) {
rateLimitStore.set(ip, entry);
return { allowed: false, remaining: 0, resetAt };
}
entry.timestamps.push(now);
rateLimitStore.set(ip, entry);
return { allowed: true, remaining: remaining - 1, resetAt };
}
// ============================================================
// Input validation
// ============================================================
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
function validateEmail(email: string): string | null {
if (!email || email.trim() === '') {
return 'Email address is required.';
}
if (!EMAIL_REGEX.test(email.trim())) {
return 'Please enter a valid email address.';
}
if (email.trim().length > 254) {
return 'Email address is too long.';
}
return null;
}
// ============================================================
// Newsletter subscription handlers
// Supports: Mailchimp, ConvertKit, or simple SMTP notification
// ============================================================
async function subscribeToNewsletter(email: string): Promise<void> {
const mailchimpApiKey = import.meta.env.MAILCHIMP_API_KEY;
const mailchimpListId = import.meta.env.MAILCHIMP_LIST_ID;
const mailchimpDataCenter = import.meta.env.MAILCHIMP_DC; // e.g. "us1"
const convertkitApiKey = import.meta.env.CONVERTKIT_API_KEY;
const convertkitFormId = import.meta.env.CONVERTKIT_FORM_ID;
// Option 1: Mailchimp integration
if (mailchimpApiKey && mailchimpListId && mailchimpDataCenter) {
const url = `https://${mailchimpDataCenter}.api.mailchimp.com/3.0/lists/${mailchimpListId}/members`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Basic ${Buffer.from(`anystring:${mailchimpApiKey}`).toString('base64')}`,
},
body: JSON.stringify({
email_address: email,
status: 'pending', // Double opt-in
tags: ['website-signup'],
}),
});
if (!response.ok) {
const error = await response.json() as { title?: string };
// Handle already-subscribed gracefully
if (error.title === 'Member Exists') {
return;
}
throw new Error(`Mailchimp error: ${error.title}`);
}
return;
}
// Option 2: ConvertKit integration
if (convertkitApiKey && convertkitFormId) {
const url = `https://api.convertkit.com/v3/forms/${convertkitFormId}/subscribe`;
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
api_key: convertkitApiKey,
email,
}),
});
if (!response.ok) {
const error = await response.json() as { message?: string };
throw new Error(`ConvertKit error: ${error.message}`);
}
return;
}
// Option 3: SMTP notification fallback
const smtpHost = import.meta.env.SMTP_HOST;
const smtpUser = import.meta.env.SMTP_USER;
const smtpPass = import.meta.env.SMTP_PASS;
const smtpPort = parseInt(import.meta.env.SMTP_PORT ?? '587', 10);
const recipientEmail = import.meta.env.CONTACT_EMAIL ?? import.meta.env.SMTP_USER;
if (smtpHost && smtpUser && smtpPass && recipientEmail) {
try {
const nodemailer = await import('nodemailer');
const transporter = nodemailer.default.createTransport({
host: smtpHost,
port: smtpPort,
secure: smtpPort === 465,
auth: { user: smtpUser, pass: smtpPass },
});
await transporter.sendMail({
from: `"WorkRoot Newsletter" <${smtpUser}>`,
to: recipientEmail,
subject: `[Newsletter] New subscriber: ${email}`,
text: `New newsletter subscription\n\nEmail: ${email}\nSubscribed at: ${new Date().toISOString()}`,
});
return;
} catch (err) {
// If nodemailer isn't installed, fall through to console log path
if (err instanceof Error && err.message.includes('Cannot find module')) {
logger.warn('api.newsletter', 'nodemailer not available — subscriber logged only', { email });
return;
}
await captureException(err, { scope: 'api.newsletter', extra: { provider: 'smtp' } });
throw new Error('Failed to process subscription');
}
}
// No integration configured — log and continue (dev/staging)
logger.info('api.newsletter', 'No provider configured — subscriber logged only', { email });
}
// ============================================================
// CORS headers helper
// ============================================================
function corsHeaders(): HeadersInit {
const origin = import.meta.env.PROD ? 'https://workroot.in' : '*';
return {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': origin,
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};
}
// ============================================================
// OPTIONS preflight handler
// ============================================================
export const OPTIONS: APIRoute = async () => {
return new Response(null, { status: 204, headers: corsHeaders() });
};
// ============================================================
// POST handler
// ============================================================
export const POST: APIRoute = async ({ request, clientAddress }) => {
const ip = clientAddress ?? 'unknown';
const startTime = Date.now();
// Rate limiting
const rateLimit = checkRateLimit(ip);
const rateLimitHeaders = {
...corsHeaders(),
'X-RateLimit-Limit': String(RATE_LIMIT_MAX_REQUESTS),
'X-RateLimit-Remaining': String(rateLimit.remaining),
'X-RateLimit-Reset': String(Math.ceil(rateLimit.resetAt / 1000)),
};
if (!rateLimit.allowed) {
logApiRequest({ scope: 'api.newsletter', method: 'POST', path: '/api/newsletter', status: 429, ip });
return new Response(
JSON.stringify({
success: false,
error: 'Too many requests. Please try again later.',
}),
{ status: 429, headers: rateLimitHeaders }
);
}
// Reject oversized payloads (max 4KB for email-only endpoint)
const contentLength = parseInt(request.headers.get('content-length') ?? '0', 10);
if (contentLength > 4096) {
return new Response(
JSON.stringify({ success: false, error: 'Request body too large.' }),
{ status: 413, headers: rateLimitHeaders }
);
}
// Parse request body
let email: string;
try {
const contentType = request.headers.get('content-type') ?? '';
if (contentType.includes('application/json')) {
const body = await request.json() as { email?: unknown };
email = String(body.email ?? '').trim();
} else {
const formData = await request.formData();
email = String(formData.get('email') ?? '').trim();
}
} catch {
return new Response(
JSON.stringify({ success: false, error: 'Invalid request body.' }),
{ status: 400, headers: rateLimitHeaders }
);
}
// Validate email
const validationError = validateEmail(email);
if (validationError) {
return new Response(
JSON.stringify({ success: false, error: validationError }),
{ status: 422, headers: rateLimitHeaders }
);
}
// Subscribe
try {
await subscribeToNewsletter(email.toLowerCase());
} catch {
logApiRequest({
scope: 'api.newsletter',
method: 'POST',
path: '/api/newsletter',
status: 500,
ip,
durationMs: Date.now() - startTime,
});
return new Response(
JSON.stringify({
success: false,
error: 'Unable to process your subscription. Please try again.',
}),
{ status: 500, headers: rateLimitHeaders }
);
}
logApiRequest({
scope: 'api.newsletter',
method: 'POST',
path: '/api/newsletter',
status: 200,
ip,
durationMs: Date.now() - startTime,
});
return new Response(
JSON.stringify({
success: true,
message: 'Thanks for subscribing! Check your inbox to confirm.',
}),
{ status: 200, headers: rateLimitHeaders }
);
};
+341
View File
@@ -0,0 +1,341 @@
---
import { getCollection, type CollectionEntry } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
import SEO from '../../components/SEO.astro';
export async function getStaticPaths() {
const posts = await getCollection('blog', ({ data }) => !data.draft);
return posts.map((post) => ({
params: { slug: post.slug },
props: { post },
}));
}
type Props = { post: CollectionEntry<'blog'> };
const { post } = Astro.props;
const { Content } = await post.render();
function getReadingTime(content: string): number {
const wordsPerMinute = 200;
const words = content.split(/\s+/).length;
return Math.ceil(words / wordsPerMinute);
}
function formatDate(date: Date): string {
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
}
const readingTime = getReadingTime(post.body);
const shareUrl = encodeURIComponent(Astro.url.href);
const shareTitle = encodeURIComponent(post.data.title);
const shareDescription = encodeURIComponent(post.data.description);
---
<BaseLayout
title={post.data.title}
description={post.data.description}
ogImage={post.data.heroImage}
>
<SEO
slot="head"
type="BlogPosting"
title={post.data.title}
description={post.data.description}
breadcrumbs={[
{ name: 'Home', url: '/' },
{ name: 'Blog', url: '/blog' },
{ name: post.data.title, url: `/blog/${post.slug}` }
]}
article={{
author: post.data.author.name,
datePublished: post.data.pubDate.toISOString(),
dateModified: post.data.updatedDate?.toISOString(),
category: post.data.category,
tags: post.data.tags,
image: post.data.heroImage,
wordCount: post.body.split(/\s+/).length
}}
/>
<article class="bg-white">
<!-- Hero Section -->
<header class="relative bg-gradient-to-br from-secondary-900 via-secondary-800 to-secondary-900 py-16 md:py-24 overflow-hidden">
<div class="absolute inset-0 opacity-20">
<div class="absolute top-10 left-10 w-64 h-64 bg-primary-500 rounded-full blur-[100px]"></div>
<div class="absolute bottom-10 right-10 w-80 h-80 bg-accent-500 rounded-full blur-[120px]"></div>
</div>
<div class="relative max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<!-- Breadcrumb -->
<nav class="mb-6" aria-label="Breadcrumb">
<ol class="flex items-center gap-2 text-sm text-secondary-400">
<li><a href="/" class="hover:text-white transition-colors">Home</a></li>
<li><span class="mx-2">/</span></li>
<li><a href="/blog" class="hover:text-white transition-colors">Blog</a></li>
<li><span class="mx-2">/</span></li>
<li class="text-secondary-300 truncate max-w-[200px]">{post.data.title}</li>
</ol>
</nav>
<!-- Category -->
<span class="inline-block px-4 py-1.5 bg-primary-500/20 text-primary-300 text-sm font-semibold rounded-full mb-4">
{post.data.category}
</span>
<!-- Title -->
<h1 class="text-3xl md:text-4xl lg:text-5xl font-bold text-white mb-6 leading-tight">
{post.data.title}
</h1>
<!-- Description -->
<p class="text-xl text-secondary-300 mb-8 max-w-3xl">
{post.data.description}
</p>
<!-- Meta -->
<div class="flex flex-wrap items-center gap-6 text-secondary-400">
<!-- Author -->
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-gradient-to-br from-primary-400 to-primary-600 flex items-center justify-center text-white font-bold">
{post.data.author.name.charAt(0)}
</div>
<div>
<p class="text-white font-medium">{post.data.author.name}</p>
<p class="text-sm">Author</p>
</div>
</div>
<!-- Divider -->
<div class="hidden sm:block w-px h-10 bg-secondary-600"></div>
<!-- Date -->
<div class="flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<time datetime={post.data.pubDate.toISOString()}>
{formatDate(post.data.pubDate)}
</time>
</div>
<!-- Reading Time -->
<div class="flex items-center gap-2">
<svg class="w-5 h-5" 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" />
</svg>
<span>{readingTime} min read</span>
</div>
</div>
</div>
</header>
<!-- Hero Image -->
{post.data.heroImage && (
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 -mt-8 relative z-10">
<img
src={post.data.heroImage}
alt={post.data.title}
width="1200"
height="675"
class="w-full aspect-video object-cover rounded-2xl shadow-2xl bg-secondary-200"
loading="eager"
fetchpriority="high"
decoding="async"
/>
</div>
)}
<!-- Content -->
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12 md:py-16">
<div class="flex gap-8">
<!-- Social Share Sidebar (Desktop) -->
<aside class="hidden lg:block w-16 flex-shrink-0">
<div class="sticky top-24 flex flex-col gap-3">
<span class="text-xs text-secondary-400 font-medium mb-1">Share</span>
<!-- Twitter/X -->
<a
href={`https://twitter.com/intent/tweet?url=${shareUrl}&text=${shareTitle}`}
target="_blank"
rel="noopener noreferrer"
class="w-10 h-10 rounded-full bg-secondary-100 hover:bg-[#1DA1F2] text-secondary-600 hover:text-white flex items-center justify-center transition-colors"
aria-label="Share on 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>
</a>
<!-- LinkedIn -->
<a
href={`https://www.linkedin.com/sharing/share-offsite/?url=${shareUrl}`}
target="_blank"
rel="noopener noreferrer"
class="w-10 h-10 rounded-full bg-secondary-100 hover:bg-[#0A66C2] text-secondary-600 hover:text-white flex items-center justify-center transition-colors"
aria-label="Share on LinkedIn"
>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
</svg>
</a>
<!-- Facebook -->
<a
href={`https://www.facebook.com/sharer/sharer.php?u=${shareUrl}`}
target="_blank"
rel="noopener noreferrer"
class="w-10 h-10 rounded-full bg-secondary-100 hover:bg-[#1877F2] text-secondary-600 hover:text-white flex items-center justify-center transition-colors"
aria-label="Share on Facebook"
>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" />
</svg>
</a>
<!-- Copy Link -->
<button
type="button"
id="copy-link-btn"
class="w-10 h-10 rounded-full bg-secondary-100 hover:bg-primary-500 text-secondary-600 hover:text-white flex items-center justify-center transition-colors"
aria-label="Copy link"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
</button>
</div>
</aside>
<!-- Main Content -->
<div class="flex-1 min-w-0">
<!-- Tags -->
<div class="flex flex-wrap gap-2 mb-8 pb-8 border-b border-secondary-200">
{post.data.tags.map((tag) => (
<span class="px-3 py-1 bg-secondary-100 text-secondary-600 text-sm rounded-full hover:bg-secondary-200 transition-colors">
#{tag}
</span>
))}
</div>
<!-- Article Content -->
<div class="prose prose-lg max-w-none prose-headings:text-secondary-900 prose-headings:font-bold prose-p:text-secondary-700 prose-a:text-primary-600 prose-a:no-underline hover:prose-a:underline prose-strong:text-secondary-900 prose-code:text-primary-700 prose-code:bg-primary-50 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:font-mono prose-code:before:content-none prose-code:after:content-none prose-pre:bg-secondary-900 prose-pre:text-secondary-100 prose-pre:rounded-xl prose-pre:shadow-lg prose-img:rounded-xl prose-img:shadow-md prose-blockquote:border-primary-500 prose-blockquote:bg-primary-50 prose-blockquote:rounded-r-lg prose-blockquote:py-1 prose-blockquote:not-italic prose-li:marker:text-primary-500">
<Content />
</div>
<!-- Mobile Social Share -->
<div class="lg:hidden mt-12 pt-8 border-t border-secondary-200">
<p class="text-sm text-secondary-500 font-medium mb-4">Share this article</p>
<div class="flex gap-3">
<a
href={`https://twitter.com/intent/tweet?url=${shareUrl}&text=${shareTitle}`}
target="_blank"
rel="noopener noreferrer"
class="flex-1 py-3 rounded-lg bg-secondary-100 hover:bg-[#1DA1F2] text-secondary-600 hover:text-white flex items-center justify-center gap-2 transition-colors"
>
<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>
<span class="text-sm font-medium">Twitter</span>
</a>
<a
href={`https://www.linkedin.com/sharing/share-offsite/?url=${shareUrl}`}
target="_blank"
rel="noopener noreferrer"
class="flex-1 py-3 rounded-lg bg-secondary-100 hover:bg-[#0A66C2] text-secondary-600 hover:text-white flex items-center justify-center gap-2 transition-colors"
>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
</svg>
<span class="text-sm font-medium">LinkedIn</span>
</a>
<button
type="button"
id="copy-link-btn-mobile"
class="flex-1 py-3 rounded-lg bg-secondary-100 hover:bg-primary-500 text-secondary-600 hover:text-white flex items-center justify-center gap-2 transition-colors"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
<span class="text-sm font-medium">Copy</span>
</button>
</div>
</div>
<!-- Author Bio -->
<div class="mt-12 p-6 bg-secondary-50 rounded-2xl">
<div class="flex items-start gap-4">
<div class="w-16 h-16 rounded-full bg-gradient-to-br from-primary-400 to-primary-600 flex items-center justify-center text-white text-2xl font-bold flex-shrink-0">
{post.data.author.name.charAt(0)}
</div>
<div>
<p class="text-sm text-secondary-500 mb-1">Written by</p>
<h3 class="text-xl font-bold text-secondary-900 mb-2">{post.data.author.name}</h3>
<p class="text-secondary-600">
A passionate technologist sharing insights on modern software development, cloud architecture, and digital innovation.
</p>
</div>
</div>
</div>
<!-- Back to Blog -->
<div class="mt-8">
<a
href="/blog"
class="inline-flex items-center gap-2 text-primary-600 hover:text-primary-700 font-medium transition-colors"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Back to all articles
</a>
</div>
</div>
</div>
</div>
</article>
</BaseLayout>
<style is:global>
/* Code blocks with JetBrains Mono */
.prose pre {
font-family: 'JetBrains Mono', monospace;
font-size: 0.875rem;
line-height: 1.7;
padding: 1.5rem;
}
.prose pre code {
background: transparent;
padding: 0;
font-family: inherit;
color: inherit;
}
/* Inline code */
.prose :not(pre) > code {
font-family: 'JetBrains Mono', monospace;
font-size: 0.875em;
}
/* Syntax highlighting colors */
.prose pre .comment { color: #6a737d; }
.prose pre .keyword { color: #ff7b72; }
.prose pre .string { color: #a5d6ff; }
.prose pre .function { color: #d2a8ff; }
.prose pre .number { color: #79c0ff; }
</style>
<script>
function copyToClipboard() {
navigator.clipboard.writeText(window.location.href).then(() => {
alert('Link copied to clipboard!');
});
}
document.getElementById('copy-link-btn')?.addEventListener('click', copyToClipboard);
document.getElementById('copy-link-btn-mobile')?.addEventListener('click', copyToClipboard);
</script>
+212
View File
@@ -0,0 +1,212 @@
---
import { getCollection } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
import SEO from '../../components/SEO.astro';
const allPosts = await getCollection('blog', ({ data }) => !data.draft);
const sortedPosts = allPosts.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
const categories = ['All', 'Web Development', 'Mobile Apps', 'AI/ML', 'Cloud', 'DevOps', 'Security'] as const;
function getReadingTime(content: string): number {
const wordsPerMinute = 200;
const words = content.split(/\s+/).length;
return Math.ceil(words / wordsPerMinute);
}
function formatDate(date: Date): string {
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
}
---
<BaseLayout
title="Blog"
description="Insights and articles on web development, AI, cloud computing, and digital transformation from the WorkRoot team."
>
<SEO
slot="head"
type="WebPage"
breadcrumbs={[
{ name: 'Home', url: '/' },
{ name: 'Blog', url: '/blog' }
]}
/>
<!-- Hero Section -->
<section class="relative bg-gradient-to-br from-secondary-900 via-secondary-800 to-secondary-900 py-20 overflow-hidden">
<div class="absolute inset-0 opacity-30">
<div class="absolute top-20 left-10 w-72 h-72 bg-primary-500 rounded-full blur-[120px]"></div>
<div class="absolute bottom-10 right-20 w-96 h-96 bg-accent-500 rounded-full blur-[150px]"></div>
</div>
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h1 class="text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-6">
Our <span class="text-transparent bg-clip-text bg-gradient-to-r from-primary-400 to-accent-400">Blog</span>
</h1>
<p class="text-xl text-secondary-300 max-w-3xl mx-auto">
Insights, tutorials, and industry trends from our team of experts. Stay ahead with the latest in technology.
</p>
</div>
</section>
<!-- Category Filter -->
<section class="bg-secondary-50 border-b border-secondary-200">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div class="flex flex-wrap gap-2 justify-center" role="tablist" aria-label="Filter posts by category">
{categories.map((category, index) => (
<button
type="button"
role="tab"
aria-selected={index === 0 ? 'true' : 'false'}
data-category={category}
class:list={[
'filter-btn px-4 py-2 rounded-full text-sm font-medium transition-all duration-200',
index === 0
? 'bg-primary-500 text-white'
: 'bg-white text-secondary-600 hover:bg-secondary-100 border border-secondary-200',
]}
>
{category}
</button>
))}
</div>
</div>
</section>
<!-- Blog Posts Grid -->
<section class="py-16 bg-white">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8" id="posts-grid">
{sortedPosts.map((post) => {
const readingTime = getReadingTime(post.body);
return (
<article
class="post-card group bg-white rounded-2xl shadow-lg overflow-hidden border border-secondary-100 hover:shadow-xl hover:border-primary-200 transition-all duration-300"
data-category={post.data.category}
>
<!-- Post Image -->
<a href={`/blog/${post.slug}`} class="block aspect-video overflow-hidden bg-secondary-200">
{post.data.heroImage ? (
<img
src={post.data.heroImage}
alt={post.data.title}
width="640"
height="360"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
loading="lazy"
decoding="async"
/>
) : (
<div class="w-full h-full flex items-center justify-center bg-gradient-to-br from-primary-500 to-primary-700">
<svg class="w-16 h-16 text-white/50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z" />
</svg>
</div>
)}
</a>
<!-- Post Content -->
<div class="p-6">
<!-- Category & Reading Time -->
<div class="flex items-center gap-3 mb-3">
<span class="px-3 py-1 bg-primary-50 text-primary-700 text-xs font-semibold rounded-full">
{post.data.category}
</span>
<span class="text-secondary-400 text-sm flex items-center gap-1">
<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="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{readingTime} min read
</span>
</div>
<!-- Title -->
<h2 class="text-xl font-bold text-secondary-900 mb-3 line-clamp-2 group-hover:text-primary-600 transition-colors">
<a href={`/blog/${post.slug}`}>{post.data.title}</a>
</h2>
<!-- Description -->
<p class="text-secondary-600 text-sm mb-4 line-clamp-3">
{post.data.description}
</p>
<!-- Tags -->
<div class="flex flex-wrap gap-2 mb-4">
{post.data.tags.slice(0, 3).map((tag) => (
<span class="text-xs text-secondary-500 bg-secondary-100 px-2 py-1 rounded">
#{tag}
</span>
))}
</div>
<!-- Author & Date -->
<div class="flex items-center justify-between pt-4 border-t border-secondary-100">
<div class="flex items-center gap-2">
<div class="w-8 h-8 rounded-full bg-gradient-to-br from-primary-400 to-primary-600 flex items-center justify-center text-white text-sm font-bold">
{post.data.author.name.charAt(0)}
</div>
<span class="text-sm text-secondary-600">{post.data.author.name}</span>
</div>
<time class="text-sm text-secondary-400" datetime={post.data.pubDate.toISOString()}>
{formatDate(post.data.pubDate)}
</time>
</div>
</div>
</article>
);
})}
</div>
<!-- Empty State -->
<div id="empty-state" class="hidden text-center py-16">
<svg class="w-16 h-16 text-secondary-300 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2z" />
</svg>
<h3 class="text-xl font-semibold text-secondary-600 mb-2">No posts found</h3>
<p class="text-secondary-400">Try selecting a different category.</p>
</div>
</div>
</section>
</BaseLayout>
<script>
const filterButtons = document.querySelectorAll('.filter-btn');
const postCards = document.querySelectorAll('.post-card');
const emptyState = document.getElementById('empty-state');
filterButtons.forEach((btn) => {
btn.addEventListener('click', () => {
const category = btn.getAttribute('data-category');
// Update button states
filterButtons.forEach((b) => {
b.classList.remove('bg-primary-500', 'text-white');
b.classList.add('bg-white', 'text-secondary-600', 'border', 'border-secondary-200');
b.setAttribute('aria-selected', 'false');
});
btn.classList.remove('bg-white', 'text-secondary-600', 'border', 'border-secondary-200');
btn.classList.add('bg-primary-500', 'text-white');
btn.setAttribute('aria-selected', 'true');
// Filter posts
let visibleCount = 0;
postCards.forEach((card) => {
const postCategory = card.getAttribute('data-category');
if (category === 'All' || postCategory === category) {
card.classList.remove('hidden');
visibleCount++;
} else {
card.classList.add('hidden');
}
});
// Show empty state if no posts
if (emptyState) {
emptyState.classList.toggle('hidden', visibleCount > 0);
}
});
});
</script>
File diff suppressed because it is too large Load Diff
+909
View File
@@ -0,0 +1,909 @@
---
// Home page - WorkRoot IT Solutions
import BaseLayout from '../layouts/BaseLayout.astro';
import SEO from '../components/SEO.astro';
// Benefits/Features data
const benefits = [
{
icon: 'rocket',
title: 'Rapid Development',
description: 'Launch your product 3x faster with our agile methodology and proven frameworks.',
},
{
icon: 'shield',
title: 'Enterprise Security',
description: 'Bank-grade security protocols protecting your data and user privacy.',
},
{
icon: 'scale',
title: 'Built to Scale',
description: 'Infrastructure that grows with your business, from startup to enterprise.',
},
{
icon: 'support',
title: '24/7 Support',
description: 'Round-the-clock technical support ensuring your systems run smoothly.',
},
];
// Services data
const services = [
{
icon: 'code',
title: 'Web Development',
description: 'Custom web applications built with modern frameworks and best practices.',
features: ['React/Next.js', 'API Development', 'Progressive Web Apps'],
},
{
icon: 'smartphone',
title: 'Mobile Apps',
description: 'Native and cross-platform mobile solutions for iOS and Android.',
features: ['iOS & Android', 'React Native', 'Flutter'],
},
{
icon: 'brain',
title: 'AI & Machine Learning',
description: 'Intelligent automation and predictive analytics for your business.',
features: ['NLP & Computer Vision', 'Predictive Analytics', 'Automation'],
},
{
icon: 'cloud',
title: 'Cloud Solutions',
description: 'Scalable cloud infrastructure and migration services.',
features: ['AWS & Azure', 'DevOps & CI/CD', 'Migration Services'],
},
];
// How it works steps
const steps = [
{
number: '01',
title: 'Discovery',
description: 'We understand your business goals, challenges, and technical requirements.',
},
{
number: '02',
title: 'Planning',
description: 'Strategic roadmap with clear milestones, timelines, and deliverables.',
},
{
number: '03',
title: 'Development',
description: 'Agile sprints with regular check-ins and iterative improvements.',
},
{
number: '04',
title: 'Launch & Support',
description: 'Smooth deployment with ongoing maintenance and optimization.',
},
];
// Testimonials data
const testimonials = [
{
quote: 'WorkRoot transformed our digital presence completely. Their team delivered beyond our expectations with a modern, scalable solution.',
author: 'Sarah Chen',
role: 'CTO, TechVentures Inc.',
avatar: 'SC',
rating: 5,
},
{
quote: 'The AI integration they built for us has increased our operational efficiency by 40%. Truly game-changing partnership.',
author: 'Michael Roberts',
role: 'CEO, DataFlow Systems',
avatar: 'MR',
rating: 5,
},
{
quote: 'Professional, responsive, and technically excellent. WorkRoot is now our go-to partner for all development needs.',
author: 'Emily Watson',
role: 'Director of IT, GlobalRetail',
avatar: 'EW',
rating: 5,
},
{
quote: 'Their cloud migration expertise saved us months of work. The transition was seamless and our costs dropped 30%.',
author: 'James Liu',
role: 'VP Engineering, CloudFirst',
avatar: 'JL',
rating: 5,
},
];
// Stats data
const stats = [
{ value: 150, suffix: '+', label: 'Projects Delivered', icon: 'briefcase' },
{ value: 98, suffix: '%', label: 'Client Satisfaction', icon: 'heart' },
{ value: 50, suffix: '+', label: 'Enterprise Clients', icon: 'building' },
{ value: 24, suffix: '/7', label: 'Support Available', icon: 'clock' },
];
// FAQ data
const faqs = [
{
question: 'What technologies do you specialize in?',
answer: 'We specialize in modern web technologies including React, Next.js, Node.js, Python, and cloud platforms like AWS and Azure. Our team stays current with the latest industry trends and best practices.',
},
{
question: 'How long does a typical project take?',
answer: 'Project timelines vary based on complexity. A simple website might take 4-6 weeks, while a complex web application could take 3-6 months. We provide detailed timelines during the planning phase.',
},
{
question: 'Do you offer ongoing support after launch?',
answer: 'Yes! We offer comprehensive support packages including bug fixes, security updates, performance monitoring, and feature enhancements. Our 24/7 support ensures your systems run smoothly.',
},
{
question: 'What is your development process?',
answer: 'We follow an agile methodology with 2-week sprints, regular check-ins, and iterative development. You\'ll have full visibility into progress with access to staging environments and regular demos.',
},
{
question: 'Can you work with our existing team?',
answer: 'Absolutely! We excel at collaborating with in-house teams. Whether you need to augment your team or prefer a fully managed solution, we adapt to your workflow and tools.',
},
{
question: 'How do you ensure project security?',
answer: 'Security is built into every phase. We follow OWASP guidelines, conduct regular security audits, implement encryption, and ensure compliance with industry standards like GDPR and SOC 2.',
},
];
---
<BaseLayout
title="Home"
description="WorkRoot IT Solutions — Build software that scales. Expert web development, mobile apps, AI/ML & cloud services. 150+ projects, 98% satisfaction, 50+ enterprise clients. Get started today."
>
<SEO
slot="head"
type="WebPage"
faq={faqs}
/>
<!-- Hero Section - Enhanced -->
<section class="relative min-h-screen flex items-center overflow-hidden bg-gradient-to-br from-secondary-900 via-secondary-800 to-primary-900">
<!-- Background decorative elements -->
<div class="absolute inset-0 overflow-hidden pointer-events-none" aria-hidden="true">
<div class="absolute -top-40 -right-40 w-[600px] h-[600px] bg-primary/20 rounded-full blur-3xl animate-pulse-slow"></div>
<div class="absolute -bottom-40 -left-40 w-[500px] h-[500px] bg-accent/10 rounded-full blur-3xl animate-pulse-slower"></div>
<div class="absolute top-1/3 right-1/4 w-[400px] h-[400px] bg-primary/10 rounded-full blur-3xl"></div>
<!-- Grid pattern overlay -->
<div class="absolute inset-0 bg-[linear-gradient(rgba(255,255,255,0.02)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.02)_1px,transparent_1px)] bg-[size:80px_80px]"></div>
<!-- Spotlight effect -->
<div class="absolute inset-0 bg-gradient-to-b from-transparent via-transparent to-secondary-900/50"></div>
</div>
<div class="container-wrapper relative z-10 py-24">
<div class="max-w-5xl mx-auto text-center">
<!-- Trust badge -->
<div class="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/10 backdrop-blur-sm border border-white/20 mb-8 opacity-0 animate-slide-up">
<svg class="w-5 h-5 text-accent-400" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
<path fill-rule="evenodd" d="M6.267 3.455a3.066 3.066 0 001.745-.723 3.066 3.066 0 013.976 0 3.066 3.066 0 001.745.723 3.066 3.066 0 012.812 2.812c.051.643.304 1.254.723 1.745a3.066 3.066 0 010 3.976 3.066 3.066 0 00-.723 1.745 3.066 3.066 0 01-2.812 2.812 3.066 3.066 0 00-1.745.723 3.066 3.066 0 01-3.976 0 3.066 3.066 0 00-1.745-.723 3.066 3.066 0 01-2.812-2.812 3.066 3.066 0 00-.723-1.745 3.066 3.066 0 010-3.976 3.066 3.066 0 00.723-1.745 3.066 3.066 0 012.812-2.812zm7.44 5.252a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg>
<span class="text-sm font-medium text-white">Trusted by 50+ Enterprise Clients</span>
</div>
<!-- Main heading with gradient -->
<h1 class="text-5xl sm:text-6xl lg:text-7xl xl:text-8xl font-bold text-white leading-[1.1] tracking-tight">
<span class="block opacity-0 animate-slide-up" style="animation-delay: 0.2s;">Build Software</span>
<span class="block opacity-0 animate-slide-up" style="animation-delay: 0.4s;">That <span class="text-transparent bg-clip-text bg-gradient-to-r from-primary-400 via-primary-300 to-accent-400">Scales</span></span>
</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;">
From startup MVP to enterprise platform, we build modern software solutions that grow with your business. Fast, secure, and built to last.
</p>
<!-- Dual CTA Buttons -->
<div class="mt-12 flex flex-wrap justify-center gap-4 opacity-0 animate-slide-up" style="animation-delay: 0.8s;">
<a
href="/contact"
class="group inline-flex items-center justify-center rounded-xl bg-gradient-to-r from-primary-500 to-primary-600 px-10 py-5 text-lg font-bold text-white transition-all hover:shadow-2xl hover:shadow-primary/50 hover:scale-105 focus:outline-none focus:ring-2 focus:ring-primary-400 focus:ring-offset-2 focus:ring-offset-secondary-900"
>
Start Your Project
<svg class="ml-2 w-5 h-5 group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3"></path>
</svg>
</a>
<a
href="/portfolio"
class="group inline-flex items-center justify-center rounded-xl border-2 border-white/30 bg-white/5 backdrop-blur-sm px-10 py-5 text-lg font-bold text-white transition-all hover:bg-white/10 hover:border-white/50 focus:outline-none focus:ring-2 focus:ring-white/50 focus:ring-offset-2 focus:ring-offset-secondary-900"
>
<svg class="mr-2 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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>
View Our Work
</a>
</div>
<!-- Social Proof Logos/Stats -->
<div class="mt-20 opacity-0 animate-slide-up" style="animation-delay: 1s;">
<p class="text-sm text-secondary-400 uppercase tracking-wider mb-8">Powered By Industry Leaders</p>
<div class="flex flex-wrap justify-center items-center gap-12 opacity-60" role="list" aria-label="Technologies we use">
<div class="text-2xl font-bold text-white" role="listitem">AWS</div>
<div class="text-2xl font-bold text-white" role="listitem">Azure</div>
<div class="text-2xl font-bold text-white" role="listitem">React</div>
<div class="text-2xl font-bold text-white" role="listitem">Next.js</div>
<div class="text-2xl font-bold text-white" role="listitem">Node.js</div>
</div>
</div>
</div>
</div>
<!-- Scroll indicator -->
<div class="absolute bottom-8 left-1/2 -translate-x-1/2 opacity-0 animate-fade-in" style="animation-delay: 1.2s;">
<a href="#benefits" class="flex flex-col items-center text-white/60 hover:text-white transition-colors group" aria-label="Scroll to benefits">
<span class="text-sm mb-2">Discover More</span>
<svg class="w-6 h-6 animate-bounce" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path>
</svg>
</a>
</div>
</section>
<!-- Benefits Section - NEW -->
<section id="benefits" class="py-24 bg-white">
<div class="container-wrapper">
<div class="text-center max-w-3xl mx-auto mb-20" data-animate="fade-up">
<span class="inline-block px-4 py-2 rounded-full bg-primary-50 text-primary-700 text-sm font-bold uppercase tracking-wider mb-4">Why Choose Us</span>
<h2 class="text-4xl sm:text-5xl font-bold text-secondary-900 mb-6">Built for Your Success</h2>
<p class="text-xl text-secondary-600">
We don't just write code. We build solutions that drive measurable business results.
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{benefits.map((benefit, index) => (
<div class="group relative bg-gradient-to-br from-secondary-50 to-white rounded-2xl p-8 border border-secondary-100 hover:border-primary/30 hover:shadow-2xl transition-all duration-500 hover:-translate-y-2 card-interactive" data-animate="fade-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 mb-6 group-hover:scale-110 group-hover:rotate-6 transition-all duration-500 shadow-lg">
{benefit.icon === 'rocket' && (
<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="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
)}
{benefit.icon === 'shield' && (
<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="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"></path>
</svg>
)}
{benefit.icon === 'scale' && (
<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="M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z"></path>
</svg>
)}
{benefit.icon === 'support' && (
<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="M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192l-3.536 3.536M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-5 0a4 4 0 11-8 0 4 4 0 018 0z"></path>
</svg>
)}
</div>
<h3 class="text-2xl font-bold text-secondary-900 mb-3">{benefit.title}</h3>
<p class="text-secondary-600 leading-relaxed">{benefit.description}</p>
<!-- Decorative corner -->
<div class="absolute top-4 right-4 w-2 h-2 bg-primary-400 rounded-full opacity-0 group-hover:opacity-100 transition-opacity"></div>
</div>
))}
</div>
</div>
</section>
<!-- Services Section - Enhanced -->
<section id="services" class="py-24 bg-gradient-to-b from-secondary-50 to-white">
<div class="container-wrapper">
<div class="text-center max-w-3xl mx-auto mb-20" data-animate="fade-up">
<span class="inline-block px-4 py-2 rounded-full bg-primary-50 text-primary-700 text-sm font-bold uppercase tracking-wider mb-4">Our Services</span>
<h2 class="text-4xl sm:text-5xl font-bold text-secondary-900 mb-6">Comprehensive Technology Solutions</h2>
<p class="text-xl text-secondary-600">
From concept to deployment, we deliver end-to-end solutions tailored to your unique business needs.
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-6xl mx-auto">
{services.map((service, index) => (
<div class="group relative bg-white rounded-3xl p-10 shadow-lg border-2 border-secondary-100 hover:border-primary-400 hover:shadow-2xl transition-all duration-500 overflow-hidden card-interactive" data-animate="fade-up" data-delay={String(index % 2 === 0 ? 0 : 150)}>
<!-- Background gradient on hover -->
<div class="absolute inset-0 bg-gradient-to-br from-primary-50/0 to-primary-50/0 group-hover:from-primary-50/50 group-hover:to-transparent transition-all duration-500"></div>
<div class="relative z-10">
<!-- Icon -->
<div class="w-20 h-20 rounded-2xl bg-gradient-to-br from-primary-500 to-primary-600 flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-500 shadow-xl">
{service.icon === 'code' && (
<svg class="w-10 h-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path>
</svg>
)}
{service.icon === 'smartphone' && (
<svg class="w-10 h-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"></path>
</svg>
)}
{service.icon === 'brain' && (
<svg class="w-10 h-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"></path>
</svg>
)}
{service.icon === 'cloud' && (
<svg class="w-10 h-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z"></path>
</svg>
)}
</div>
<h3 class="text-2xl font-bold text-secondary-900 mb-4">{service.title}</h3>
<p class="text-secondary-600 text-lg mb-6 leading-relaxed">{service.description}</p>
<!-- Features list -->
<ul class="space-y-3">
{service.features?.map((feature) => (
<li class="flex items-center text-secondary-700">
<svg class="w-5 h-5 text-primary-500 mr-3 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg>
<span class="font-medium">{feature}</span>
</li>
))}
</ul>
<!-- Learn more arrow -->
<div class="mt-6 flex items-center text-primary-600 font-semibold group-hover:text-primary-700 transition-colors">
<span>Learn More</span>
<svg class="w-5 h-5 ml-2 group-hover:translate-x-2 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3"></path>
</svg>
</div>
</div>
<!-- Number badge -->
<div class="absolute top-6 right-6 w-12 h-12 rounded-full bg-primary-100 flex items-center justify-center text-primary-700 font-bold text-lg opacity-50 group-hover:opacity-100 transition-opacity">
{(index + 1).toString().padStart(2, '0')}
</div>
</div>
))}
</div>
<div class="text-center mt-16">
<a href="/services" class="inline-flex items-center justify-center rounded-xl bg-primary px-10 py-5 text-lg font-bold text-white transition-all hover:bg-primary-600 hover:shadow-2xl hover:shadow-primary/30 hover:scale-105 focus:outline-none focus:ring-2 focus:ring-primary-400 focus:ring-offset-2">
Explore All Services
<svg class="ml-2 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3"></path>
</svg>
</a>
</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 -->
<div class="absolute inset-0 opacity-5">
<div class="absolute inset-0 bg-[linear-gradient(rgba(255,255,255,0.1)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.1)_1px,transparent_1px)] bg-[size:64px_64px]"></div>
</div>
<div class="container-wrapper relative z-10">
<div class="text-center max-w-3xl mx-auto mb-20">
<span class="inline-block px-4 py-2 rounded-full bg-primary-400/20 text-primary-300 text-sm font-bold uppercase tracking-wider mb-4">Our Process</span>
<h2 class="text-4xl sm:text-5xl font-bold text-white mb-6">How We Work</h2>
<p class="text-xl text-secondary-300">
A proven methodology that delivers results, on time and on budget.
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 max-w-7xl mx-auto">
{steps.map((step, index) => (
<div class="relative" data-animate="fade-up" data-delay={String(index * 150)}>
{/* Connecting line */}
{index < steps.length - 1 && (
<div class="hidden lg:block absolute top-20 left-[60%] w-full h-0.5 bg-gradient-to-r from-primary-500/50 to-transparent"></div>
)}
<div class="group relative bg-white/5 backdrop-blur-sm rounded-2xl p-8 border border-white/10 hover:bg-white/10 hover:border-primary-400/50 transition-all duration-500">
{/* Number badge */}
<div class="w-16 h-16 rounded-2xl bg-gradient-to-br from-primary-500 to-primary-600 flex items-center justify-center mb-6 shadow-lg group-hover:scale-110 transition-transform duration-500">
<span class="text-2xl font-bold text-white">{step.number}</span>
</div>
<h3 class="text-2xl font-bold text-white mb-4">{step.title}</h3>
<p class="text-secondary-300 leading-relaxed">{step.description}</p>
{/* Decorative dot */}
<div class="absolute bottom-6 right-6 w-2 h-2 bg-primary-400 rounded-full opacity-0 group-hover:opacity-100 transition-opacity"></div>
</div>
</div>
))}
</div>
<div class="text-center mt-16">
<a href="/contact" class="inline-flex items-center justify-center rounded-xl bg-white px-10 py-5 text-lg font-bold text-secondary-900 transition-all hover:shadow-2xl hover:scale-105 focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-secondary-900">
Start Your Journey
<svg class="ml-2 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3"></path>
</svg>
</a>
</div>
</div>
</section>
<!-- Stats/Metrics Section - Enhanced -->
<section class="py-24 bg-white">
<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 text-accent-700 text-sm font-bold uppercase tracking-wider mb-4">Track Record</span>
<h2 class="text-4xl sm:text-5xl font-bold text-secondary-900 mb-6">Numbers That Matter</h2>
<p class="text-xl text-secondary-600">
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 rounded-3xl p-10 border-2 border-secondary-100 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 === 'heart' && (
<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.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"></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 mb-2" data-target={stat.value} data-suffix={stat.suffix}>
0{stat.suffix}
</div>
<div class="text-secondary-600 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">
<div class="container-wrapper">
<div class="text-center max-w-3xl mx-auto mb-20" data-animate="fade-up">
<span class="inline-block px-4 py-2 rounded-full bg-accent-50 text-accent-700 text-sm font-bold uppercase tracking-wider mb-4">Client Stories</span>
<h2 class="text-4xl sm:text-5xl font-bold text-secondary-900 mb-6">Loved by Teams Worldwide</h2>
<p class="text-xl text-secondary-600">
Don't just take our word for it. Here's what our clients have to say about working with us.
</p>
</div>
<!-- Testimonials Carousel -->
<div class="relative max-w-5xl mx-auto" data-animate="fade-up" data-delay="200">
<div class="testimonial-carousel overflow-hidden rounded-3xl" aria-roledescription="carousel" aria-label="Client testimonials">
<div class="testimonial-track flex transition-transform duration-500 ease-in-out" aria-live="polite" aria-atomic="false">
{testimonials.map((testimonial, index) => (
<div class="testimonial-slide w-full flex-shrink-0 px-4" data-index={index} role="group" aria-roledescription="slide" aria-label={`Testimonial ${index + 1} of ${testimonials.length}`}>
<div class="bg-gradient-to-br from-white to-secondary-50 rounded-3xl p-10 sm:p-14 border-2 border-secondary-100 shadow-xl">
<!-- Star rating -->
<div class="flex gap-1 mb-6" aria-label={`${testimonial.rating || 5} out of 5 stars`}>
{[...Array(testimonial.rating || 5)].map(() => (
<svg class="w-6 h-6 text-accent-500" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path>
</svg>
))}
</div>
<!-- Quote icon -->
<svg class="w-16 h-16 text-primary-500/20 mb-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M14.017 21v-7.391c0-5.704 3.731-9.57 8.983-10.609l.995 2.151c-2.432.917-3.995 3.638-3.995 5.849h4v10h-9.983zm-14.017 0v-7.391c0-5.704 3.748-9.57 9-10.609l.996 2.151c-2.433.917-3.996 3.638-3.996 5.849h3.983v10h-9.983z"></path>
</svg>
<blockquote class="text-2xl sm:text-3xl text-secondary-800 font-medium leading-relaxed mb-10">
"{testimonial.quote}"
</blockquote>
<div class="flex items-center">
<div class="w-16 h-16 rounded-full bg-gradient-to-br from-primary-500 to-primary-600 flex items-center justify-center text-white font-bold text-xl shadow-lg" aria-hidden="true">
{testimonial.avatar}
</div>
<div class="ml-5">
<div class="font-bold text-secondary-900 text-lg">{testimonial.author}</div>
<div class="text-secondary-600">{testimonial.role}</div>
</div>
</div>
</div>
</div>
))}
</div>
</div>
<!-- Carousel Controls -->
<div class="flex items-center justify-center mt-10 gap-6">
<button
class="carousel-prev w-14 h-14 rounded-full bg-white border-2 border-secondary-200 flex items-center justify-center text-secondary-600 hover:border-primary hover:text-primary hover:bg-primary-50 transition-all shadow-lg hover:shadow-xl"
aria-label="Previous testimonial"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M15 19l-7-7 7-7"></path>
</svg>
</button>
<!-- Dots indicator -->
<div class="carousel-dots flex gap-3">
{testimonials.map((_, index) => (
<button
class={`h-3 rounded-full transition-all ${index === 0 ? 'bg-primary w-10' : 'bg-secondary-300 hover:bg-secondary-400 w-3'}`}
data-index={index}
aria-label={`Go to testimonial ${index + 1}`}
>
</button>
))}
</div>
<button
class="carousel-next w-14 h-14 rounded-full bg-white border-2 border-secondary-200 flex items-center justify-center text-secondary-600 hover:border-primary hover:text-primary hover:bg-primary-50 transition-all shadow-lg hover:shadow-xl"
aria-label="Next testimonial"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M9 5l7 7-7 7"></path>
</svg>
</button>
</div>
</div>
</div>
</section>
<!-- FAQ Section - NEW -->
<section class="py-24 bg-white">
<div class="container-wrapper">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-16 items-start max-w-7xl mx-auto">
<!-- Left column - Header -->
<div class="lg:sticky lg:top-8" data-animate="fade-right">
<span class="inline-block px-4 py-2 rounded-full bg-primary-50 text-primary-700 text-sm font-bold uppercase tracking-wider mb-4">FAQ</span>
<h2 class="text-4xl sm:text-5xl font-bold text-secondary-900 mb-6">Common Questions</h2>
<p class="text-xl text-secondary-600 mb-8">
Everything you need to know about working with us. Can't find the answer you're looking for?
</p>
<a href="/contact" class="inline-flex items-center text-primary-600 font-semibold hover:text-primary-700 transition-colors">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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"></path>
</svg>
Chat with our team
</a>
</div>
<!-- Right column - Accordion -->
<div class="space-y-4" data-animate="fade-left" data-delay="150">
{faqs.map((faq, index) => (
<div class="group bg-secondary-50 rounded-2xl border-2 border-secondary-100 hover:border-primary-300 transition-all duration-300 overflow-hidden">
<button
class="faq-question w-full text-left px-8 py-6 flex items-start justify-between gap-4"
data-faq-index={index}
aria-expanded="false"
aria-controls={`faq-answer-${index}`}
id={`faq-question-${index}`}
>
<span class="font-bold text-lg text-secondary-900 pr-4">{faq.question}</span>
<svg class="faq-icon w-6 h-6 text-primary-500 flex-shrink-0 transition-transform duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7"></path>
</svg>
</button>
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300" id={`faq-answer-${index}`} role="region" aria-labelledby={`faq-question-${index}`}>
<div class="px-8 pb-6 text-secondary-600 leading-relaxed">
{faq.answer}
</div>
</div>
</div>
))}
</div>
</div>
</div>
</section>
<!-- Final CTA Section - Enhanced -->
<section class="py-24 bg-gradient-to-br from-secondary-900 via-primary-900 to-primary-800 relative overflow-hidden">
<!-- Background decorations -->
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<div class="absolute -top-40 -right-40 w-[500px] h-[500px] bg-accent/20 rounded-full blur-3xl"></div>
<div class="absolute -bottom-40 -left-40 w-[500px] h-[500px] bg-primary/20 rounded-full blur-3xl"></div>
<div class="absolute inset-0 bg-[linear-gradient(rgba(255,255,255,0.03)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.03)_1px,transparent_1px)] bg-[size:64px_64px]"></div>
</div>
<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 Build Something <span class="text-transparent bg-clip-text bg-gradient-to-r from-primary-300 to-accent-400">Amazing?</span>
</h2>
<p class="text-xl sm:text-2xl text-secondary-200 mb-12 max-w-2xl mx-auto leading-relaxed">
Let's turn your vision into reality. Get in touch and we'll schedule a free consultation to discuss your project.
</p>
<div class="flex flex-wrap justify-center gap-6">
<a
href="/contact"
class="group inline-flex items-center justify-center rounded-xl bg-white px-12 py-6 text-lg font-bold text-secondary-900 transition-all hover:shadow-2xl hover:shadow-white/30 hover:scale-105 focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-primary-900"
>
Get Started Today
<svg class="ml-2 w-5 h-5 group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M17 8l4 4m0 0l-4 4m4-4H3"></path>
</svg>
</a>
<a
href="/portfolio"
class="inline-flex items-center justify-center rounded-xl border-2 border-white/30 bg-white/5 backdrop-blur-sm px-12 py-6 text-lg font-bold text-white transition-all hover:bg-white/10 hover:border-white/50 focus:outline-none focus:ring-2 focus:ring-white/50 focus:ring-offset-2 focus:ring-offset-primary-900"
>
<svg class="mr-2 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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>
View Case Studies
</a>
</div>
<!-- Contact info -->
<div class="mt-16 pt-12 border-t border-white/10">
<p class="text-secondary-300 mb-6">Prefer to reach out directly?</p>
<div class="flex flex-wrap justify-center gap-8 text-white">
<a href="mailto:info@workroot.in" class="flex items-center gap-2 hover:text-primary-300 transition-colors">
<svg class="w-5 h-5" 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"></path>
</svg>
<span class="font-semibold">info@workroot.in</span>
</a>
<a href="tel:+918860077551" class="flex items-center gap-2 hover:text-primary-300 transition-colors">
<svg class="w-5 h-5" 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"></path>
</svg>
<span class="font-semibold">+91 88600 77551</span>
</a>
</div>
</div>
</div>
</div>
</section>
</BaseLayout>
<style>
/* Animation keyframes */
@keyframes slide-up {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes pulse-slow {
0%, 100% {
opacity: 0.4;
}
50% {
opacity: 0.6;
}
}
@keyframes pulse-slower {
0%, 100% {
opacity: 0.2;
}
50% {
opacity: 0.4;
}
}
.animate-slide-up {
animation: slide-up 0.8s ease-out forwards;
}
.animate-fade-in {
animation: fade-in 0.8s ease-out forwards;
}
.animate-pulse-slow {
animation: pulse-slow 4s ease-in-out infinite;
}
.animate-pulse-slower {
animation: pulse-slower 6s ease-in-out infinite;
}
/* Stat counter animation */
.stat-counter {
font-variant-numeric: tabular-nums;
}
/* FAQ accordion styles */
.faq-answer {
transition: max-height 0.3s ease-out;
}
.faq-question[aria-expanded="true"] + .faq-answer {
max-height: 500px;
}
.faq-question[aria-expanded="true"] .faq-icon {
transform: rotate(180deg);
}
</style>
<script>
// Stats counter animation
function animateCounters() {
const counters = document.querySelectorAll('.stat-counter');
const duration = 2000;
counters.forEach((counter) => {
const target = parseInt(counter.getAttribute('data-target') || '0');
const suffix = counter.getAttribute('data-suffix') || '';
const startTime = performance.now();
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 + suffix;
if (progress < 1) {
requestAnimationFrame(updateCounter);
}
}
requestAnimationFrame(updateCounter);
});
}
// Testimonials carousel
function initCarousel() {
const track = document.querySelector('.testimonial-track') as HTMLElement;
const slides = document.querySelectorAll('.testimonial-slide');
const dots = document.querySelectorAll('.carousel-dots button');
const prevBtn = document.querySelector('.carousel-prev');
const nextBtn = document.querySelector('.carousel-next');
if (!track || slides.length === 0) return;
let currentIndex = 0;
let autoplayInterval: ReturnType<typeof setInterval>;
const totalSlides = slides.length;
const autoplayDelay = 6000;
function goToSlide(index: number) {
currentIndex = (index + totalSlides) % totalSlides;
track.style.transform = `translateX(-${currentIndex * 100}%)`;
dots.forEach((dot, i) => {
if (i === currentIndex) {
dot.classList.remove('bg-secondary-300', 'hover:bg-secondary-400', 'w-3');
dot.classList.add('bg-primary', 'w-10');
} else {
dot.classList.add('bg-secondary-300', 'hover:bg-secondary-400', 'w-3');
dot.classList.remove('bg-primary', 'w-10');
}
});
}
function nextSlide() {
goToSlide(currentIndex + 1);
}
function prevSlide() {
goToSlide(currentIndex - 1);
}
function startAutoplay() {
autoplayInterval = setInterval(nextSlide, autoplayDelay);
}
function stopAutoplay() {
clearInterval(autoplayInterval);
}
function resetAutoplay() {
stopAutoplay();
startAutoplay();
}
// Event listeners
prevBtn?.addEventListener('click', () => {
prevSlide();
resetAutoplay();
});
nextBtn?.addEventListener('click', () => {
nextSlide();
resetAutoplay();
});
dots.forEach((dot, index) => {
dot.addEventListener('click', () => {
goToSlide(index);
resetAutoplay();
});
});
// Pause on hover
track.addEventListener('mouseenter', stopAutoplay);
track.addEventListener('mouseleave', startAutoplay);
// Start autoplay
startAutoplay();
}
// FAQ Accordion
function initFAQ() {
const faqButtons = document.querySelectorAll('.faq-question');
faqButtons.forEach((button) => {
button.addEventListener('click', () => {
const isExpanded = button.getAttribute('aria-expanded') === 'true';
// Close all FAQs
faqButtons.forEach((btn) => {
btn.setAttribute('aria-expanded', 'false');
});
// Toggle current FAQ
if (!isExpanded) {
button.setAttribute('aria-expanded', 'true');
}
});
});
}
// Intersection Observer for counter animation
const statsSection = document.querySelector('.stat-counter')?.closest('section');
if (statsSection) {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
animateCounters();
observer.disconnect();
}
});
},
{ threshold: 0.3 }
);
observer.observe(statsSection);
}
// Initialize components
initCarousel();
initFAQ();
// Smooth scroll for anchor links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
const href = this.getAttribute('href');
if (href && href !== '#') {
e.preventDefault();
const target = document.querySelector(href);
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
}
});
});
</script>
+82
View File
@@ -0,0 +1,82 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
---
<BaseLayout
title="You're Offline"
description="It looks like you're offline. Please check your internet connection."
noIndex={true}
>
<section class="min-h-[60vh] flex items-center justify-center px-4">
<div class="text-center max-w-lg mx-auto">
<!-- Icon -->
<div class="mb-8 flex justify-center">
<div class="w-24 h-24 rounded-full bg-primary/10 flex items-center justify-center">
<svg
class="w-12 h-12 text-primary"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M18.364 5.636a9 9 0 010 12.728M15.536 8.464a5 5 0 010 7.072M12 12h.01M6.343 17.657a9 9 0 010-12.728M8.464 15.536a5 5 0 010-7.072"
/>
<line x1="3" y1="3" x2="21" y2="21" stroke-width="1.5" stroke-linecap="round" />
</svg>
</div>
</div>
<!-- Heading -->
<h1 class="text-3xl font-bold text-secondary-900 mb-4">You're Offline</h1>
<!-- Message -->
<p class="text-secondary-600 text-lg mb-8">
It looks like you've lost your internet connection. Some content may still be available
from your last visit.
</p>
<!-- Actions -->
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<button
id="retry-btn"
class="btn-primary px-8 py-3 rounded-lg font-semibold transition-all"
onclick="window.location.reload()"
>
Try Again
</button>
<a
href="/"
class="btn-secondary px-8 py-3 rounded-lg font-semibold transition-all text-center"
>
Go to Home
</a>
</div>
<!-- Cached pages hint -->
<p class="mt-8 text-sm text-secondary-400">
Previously visited pages may still be accessible from your browser cache.
</p>
</div>
</section>
</BaseLayout>
<style>
.btn-primary {
background-color: theme('colors.primary.DEFAULT');
color: white;
}
.btn-primary:hover {
background-color: theme('colors.primary.600');
}
.btn-secondary {
border: 2px solid theme('colors.primary.DEFAULT');
color: theme('colors.primary.DEFAULT');
}
.btn-secondary:hover {
background-color: theme('colors.primary.50');
}
</style>
File diff suppressed because it is too large Load Diff
+224
View File
@@ -0,0 +1,224 @@
---
// Privacy Policy Page for WorkRoot IT Solutions
// Features: Legal content with proper formatting for readability
import BaseLayout from '../layouts/BaseLayout.astro';
const lastUpdated = 'March 20, 2026';
---
<BaseLayout
title="Privacy Policy"
description="WorkRoot IT Solutions Privacy Policy - Learn how we collect, use, and protect your personal information."
noIndex={true}
>
<!-- Hero Section -->
<section class="relative bg-gradient-to-br from-secondary-900 via-secondary-800 to-secondary-900 py-16 lg:py-20">
<div class="absolute inset-0 overflow-hidden">
<div class="absolute top-0 left-1/4 w-96 h-96 bg-primary-500/10 rounded-full blur-3xl"></div>
<div class="absolute bottom-0 right-1/4 w-96 h-96 bg-accent-500/10 rounded-full blur-3xl"></div>
</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">
<h1 class="text-4xl md:text-5xl font-bold text-white mb-4">
Privacy <span class="text-transparent bg-clip-text bg-gradient-to-r from-primary-400 to-accent-400">Policy</span>
</h1>
<p class="text-lg text-secondary-300">
Last updated: {lastUpdated}
</p>
</div>
</div>
</section>
<!-- Content Section -->
<section class="py-16 lg:py-24 bg-secondary-950">
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="max-w-4xl mx-auto">
<div class="prose prose-lg prose-invert prose-headings:text-white prose-p:text-secondary-300 prose-li:text-secondary-300 prose-strong:text-white max-w-none">
<!-- Introduction -->
<div class="bg-secondary-900/50 border border-secondary-800 rounded-2xl p-8 mb-8">
<p class="text-secondary-300 leading-relaxed">
At WorkRoot IT Solutions ("we," "our," or "us"), we are committed to protecting your privacy and ensuring the security of your personal information. This Privacy Policy explains how we collect, use, disclose, and safeguard your information when you visit our website or use our services.
</p>
</div>
<!-- Section 1 -->
<div class="mb-12">
<h2 class="text-2xl font-bold text-white mb-4 flex items-center gap-3">
<span class="flex items-center justify-center w-10 h-10 rounded-full bg-primary-500/20 text-primary-400 text-lg font-bold">1</span>
Information We Collect
</h2>
<div class="pl-13 space-y-4">
<h3 class="text-xl font-semibold text-white mt-6 mb-3">Personal Information</h3>
<p class="text-secondary-300">We may collect personal information that you voluntarily provide to us, including:</p>
<ul class="list-disc pl-6 space-y-2 text-secondary-300">
<li>Name and contact information (email address, phone number, mailing address)</li>
<li>Company name and job title</li>
<li>Account credentials (username and password)</li>
<li>Payment and billing information</li>
<li>Communication preferences</li>
</ul>
<h3 class="text-xl font-semibold text-white mt-6 mb-3">Automatically Collected Information</h3>
<p class="text-secondary-300">When you visit our website, we may automatically collect:</p>
<ul class="list-disc pl-6 space-y-2 text-secondary-300">
<li>IP address and device identifiers</li>
<li>Browser type and operating system</li>
<li>Pages visited and time spent on our site</li>
<li>Referring website addresses</li>
<li>Cookies and similar tracking technologies</li>
</ul>
</div>
</div>
<!-- Section 2 -->
<div class="mb-12">
<h2 class="text-2xl font-bold text-white mb-4 flex items-center gap-3">
<span class="flex items-center justify-center w-10 h-10 rounded-full bg-primary-500/20 text-primary-400 text-lg font-bold">2</span>
How We Use Your Information
</h2>
<div class="pl-13">
<p class="text-secondary-300 mb-4">We use the information we collect to:</p>
<ul class="list-disc pl-6 space-y-2 text-secondary-300">
<li>Provide, operate, and maintain our services</li>
<li>Process transactions and send related information</li>
<li>Respond to your inquiries and provide customer support</li>
<li>Send promotional communications (with your consent)</li>
<li>Improve our website and services</li>
<li>Detect and prevent fraud or security threats</li>
<li>Comply with legal obligations</li>
</ul>
</div>
</div>
<!-- Section 3 -->
<div class="mb-12">
<h2 class="text-2xl font-bold text-white mb-4 flex items-center gap-3">
<span class="flex items-center justify-center w-10 h-10 rounded-full bg-primary-500/20 text-primary-400 text-lg font-bold">3</span>
Information Sharing and Disclosure
</h2>
<div class="pl-13">
<p class="text-secondary-300 mb-4">We may share your information with:</p>
<ul class="list-disc pl-6 space-y-2 text-secondary-300">
<li><strong class="text-white">Service Providers:</strong> Third parties who perform services on our behalf (hosting, analytics, payment processing)</li>
<li><strong class="text-white">Business Partners:</strong> Trusted partners for joint marketing initiatives (with your consent)</li>
<li><strong class="text-white">Legal Requirements:</strong> When required by law or to protect our rights</li>
<li><strong class="text-white">Business Transfers:</strong> In connection with a merger, acquisition, or sale of assets</li>
</ul>
<p class="text-secondary-300 mt-4">We do not sell your personal information to third parties.</p>
</div>
</div>
<!-- Section 4 -->
<div class="mb-12">
<h2 class="text-2xl font-bold text-white mb-4 flex items-center gap-3">
<span class="flex items-center justify-center w-10 h-10 rounded-full bg-primary-500/20 text-primary-400 text-lg font-bold">4</span>
Cookies and Tracking Technologies
</h2>
<div class="pl-13">
<p class="text-secondary-300 mb-4">We use cookies and similar technologies to:</p>
<ul class="list-disc pl-6 space-y-2 text-secondary-300">
<li>Remember your preferences and settings</li>
<li>Analyze website traffic and usage patterns</li>
<li>Deliver targeted advertising</li>
<li>Improve user experience</li>
</ul>
<p class="text-secondary-300 mt-4">You can manage cookie preferences through your browser settings. Note that disabling cookies may affect website functionality.</p>
</div>
</div>
<!-- Section 5 -->
<div class="mb-12">
<h2 class="text-2xl font-bold text-white mb-4 flex items-center gap-3">
<span class="flex items-center justify-center w-10 h-10 rounded-full bg-primary-500/20 text-primary-400 text-lg font-bold">5</span>
Data Security
</h2>
<div class="pl-13">
<p class="text-secondary-300">We implement appropriate technical and organizational measures to protect your personal information, including:</p>
<ul class="list-disc pl-6 space-y-2 text-secondary-300 mt-4">
<li>SSL/TLS encryption for data in transit</li>
<li>Secure data storage with access controls</li>
<li>Regular security assessments and updates</li>
<li>Employee training on data protection</li>
</ul>
<p class="text-secondary-300 mt-4">While we strive to protect your information, no method of transmission over the Internet is 100% secure.</p>
</div>
</div>
<!-- Section 6 -->
<div class="mb-12">
<h2 class="text-2xl font-bold text-white mb-4 flex items-center gap-3">
<span class="flex items-center justify-center w-10 h-10 rounded-full bg-primary-500/20 text-primary-400 text-lg font-bold">6</span>
Your Rights and Choices
</h2>
<div class="pl-13">
<p class="text-secondary-300 mb-4">Depending on your location, you may have the right to:</p>
<ul class="list-disc pl-6 space-y-2 text-secondary-300">
<li>Access and obtain a copy of your personal data</li>
<li>Correct inaccurate or incomplete information</li>
<li>Request deletion of your personal data</li>
<li>Object to or restrict certain processing</li>
<li>Data portability (receive your data in a structured format)</li>
<li>Withdraw consent at any time</li>
<li>Opt-out of marketing communications</li>
</ul>
<p class="text-secondary-300 mt-4">To exercise these rights, please contact us using the information below.</p>
</div>
</div>
<!-- Section 7 -->
<div class="mb-12">
<h2 class="text-2xl font-bold text-white mb-4 flex items-center gap-3">
<span class="flex items-center justify-center w-10 h-10 rounded-full bg-primary-500/20 text-primary-400 text-lg font-bold">7</span>
Data Retention
</h2>
<div class="pl-13">
<p class="text-secondary-300">We retain your personal information for as long as necessary to fulfill the purposes outlined in this Privacy Policy, unless a longer retention period is required or permitted by law. When we no longer need your information, we will securely delete or anonymize it.</p>
</div>
</div>
<!-- Section 8 -->
<div class="mb-12">
<h2 class="text-2xl font-bold text-white mb-4 flex items-center gap-3">
<span class="flex items-center justify-center w-10 h-10 rounded-full bg-primary-500/20 text-primary-400 text-lg font-bold">8</span>
Children's Privacy
</h2>
<div class="pl-13">
<p class="text-secondary-300">Our services are not intended for individuals under the age of 16. We do not knowingly collect personal information from children. If you believe we have collected information from a child, please contact us immediately.</p>
</div>
</div>
<!-- Section 9 -->
<div class="mb-12">
<h2 class="text-2xl font-bold text-white mb-4 flex items-center gap-3">
<span class="flex items-center justify-center w-10 h-10 rounded-full bg-primary-500/20 text-primary-400 text-lg font-bold">9</span>
Changes to This Policy
</h2>
<div class="pl-13">
<p class="text-secondary-300">We may update this Privacy Policy from time to time. We will notify you of any material changes by posting the new policy on this page and updating the "Last updated" date. Your continued use of our services after such changes constitutes acceptance of the updated policy.</p>
</div>
</div>
<!-- Section 10 -->
<div class="mb-12">
<h2 class="text-2xl font-bold text-white mb-4 flex items-center gap-3">
<span class="flex items-center justify-center w-10 h-10 rounded-full bg-primary-500/20 text-primary-400 text-lg font-bold">10</span>
Contact Us
</h2>
<div class="pl-13">
<p class="text-secondary-300 mb-4">If you have questions about this Privacy Policy or our data practices, please contact us:</p>
<div class="bg-secondary-900/50 border border-secondary-800 rounded-xl p-6">
<p class="text-secondary-300"><strong class="text-white">WorkRoot IT Solutions</strong></p>
<p class="text-secondary-300">Email: privacy@workroot.in</p>
<p class="text-secondary-300">Phone: +1 (555) 123-4567</p>
<p class="text-secondary-300">Address: 123 Tech Avenue, Suite 500, San Francisco, CA 94102</p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</BaseLayout>
+949
View File
@@ -0,0 +1,949 @@
---
// Services Page for WorkRoot IT Solutions
// Enhanced: Service cards with tech stack, pricing tiers, case study highlights, scroll animations
import BaseLayout from '../layouts/BaseLayout.astro';
import SEO from '../components/SEO.astro';
// Service data with extended details
const services = [
{
id: 'web-development',
title: 'Web Development',
shortDescription: 'Custom web applications built with modern frameworks for performance and scalability.',
fullDescription: 'From responsive websites to complex web applications, we deliver solutions that drive results. Our team specializes in React, Next.js, and Node.js to build fast, secure, and maintainable web experiences that convert visitors into customers.',
icon: 'web',
features: [
'Custom web application development',
'E-commerce solutions',
'Progressive Web Apps (PWA)',
'API development & integration',
'Performance optimization',
'SEO-friendly architecture',
],
techStack: ['React', 'Next.js', 'Node.js', 'TypeScript', 'PostgreSQL'],
stats: { projects: '80+', clients: '50+', uptime: '99.9%' },
gradient: 'from-primary-500 to-primary-700',
bgLight: 'bg-primary-50',
iconColor: 'text-primary-500',
accentBorder: 'border-primary-500',
tagBg: 'bg-primary-100 text-primary-700',
},
{
id: 'mobile-development',
title: 'Mobile Development',
shortDescription: 'Native and cross-platform mobile apps that deliver exceptional user experiences.',
fullDescription: 'We create mobile applications that users love. Whether you need iOS, Android, or cross-platform solutions, our developers use React Native and Flutter to build apps that perform flawlessly across all devices and screen sizes.',
icon: 'mobile',
features: [
'iOS & Android native apps',
'Cross-platform development',
'UI/UX design for mobile',
'App Store optimization',
'Push notifications & analytics',
'Offline-first architecture',
],
techStack: ['React Native', 'Flutter', 'Swift', 'Kotlin', 'Firebase'],
stats: { projects: '40+', clients: '30+', rating: '4.8★' },
gradient: 'from-accent-500 to-accent-700',
bgLight: 'bg-accent-50',
iconColor: 'text-accent-500',
accentBorder: 'border-accent-500',
tagBg: 'bg-amber-100 text-amber-700',
},
{
id: 'ai-ml',
title: 'AI & Machine Learning',
shortDescription: 'Intelligent solutions powered by cutting-edge artificial intelligence technologies.',
fullDescription: 'Harness the power of AI to transform your business. From predictive analytics to natural language processing, we implement machine learning solutions that provide actionable insights and automate complex processes at scale.',
icon: 'ai',
features: [
'Predictive analytics',
'Natural Language Processing',
'Computer vision solutions',
'Recommendation engines',
'Chatbots & virtual assistants',
'ML model deployment & monitoring',
],
techStack: ['Python', 'TensorFlow', 'PyTorch', 'OpenAI API', 'LangChain'],
stats: { models: '25+', accuracy: '94%', savings: '60%' },
gradient: 'from-emerald-500 to-emerald-700',
bgLight: 'bg-emerald-50',
iconColor: 'text-emerald-600',
accentBorder: 'border-emerald-500',
tagBg: 'bg-emerald-100 text-emerald-700',
},
{
id: 'cloud-services',
title: 'Cloud Services',
shortDescription: 'Scalable cloud infrastructure and DevOps solutions for modern businesses.',
fullDescription: 'Migrate, scale, and optimize with confidence. Our cloud experts help you leverage AWS, Azure, and Google Cloud to build resilient, cost-effective infrastructure that grows with your business and adapts to any load.',
icon: 'cloud',
features: [
'Cloud migration & strategy',
'Infrastructure as Code (IaC)',
'Kubernetes & container orchestration',
'CI/CD pipeline setup',
'Cloud security & compliance',
'24/7 monitoring & support',
],
techStack: ['AWS', 'Azure', 'GCP', 'Kubernetes', 'Terraform'],
stats: { migrations: '35+', savings: '40%', uptime: '99.99%' },
gradient: 'from-secondary-600 to-secondary-800',
bgLight: 'bg-secondary-50',
iconColor: 'text-secondary-600',
accentBorder: 'border-secondary-500',
tagBg: 'bg-slate-100 text-slate-700',
},
];
// Pricing tiers
const pricingTiers = [
{
name: 'Starter',
price: '$2,500',
period: 'per project',
description: 'Perfect for startups and small businesses launching their digital presence.',
features: [
'Up to 5 pages / screens',
'Responsive design',
'Basic SEO setup',
'Contact form integration',
'1 month post-launch support',
'Source code handoff',
],
cta: 'Get Started',
highlighted: false,
},
{
name: 'Growth',
price: '$8,500',
period: 'per project',
description: 'For businesses ready to scale with advanced features and integrations.',
features: [
'Unlimited pages / screens',
'Custom UI/UX design',
'API & third-party integrations',
'Performance optimization',
'3 months post-launch support',
'Analytics dashboard',
'Priority support',
],
cta: 'Most Popular',
highlighted: true,
},
{
name: 'Enterprise',
price: 'Custom',
period: 'tailored quote',
description: 'Full-scale solutions for complex, high-traffic enterprise applications.',
features: [
'End-to-end architecture',
'Dedicated development team',
'SLA-backed uptime',
'Security audits & compliance',
'12 months support & maintenance',
'Training & documentation',
'White-glove onboarding',
],
cta: 'Contact Sales',
highlighted: false,
},
];
// Case study highlights
const caseStudies = [
{
service: 'Web Development',
client: 'FinTech Platform',
result: '3x faster load times, 45% increase in conversion rate',
description: 'Rebuilt a legacy banking dashboard with Next.js and real-time WebSocket updates, cutting page load from 8s to under 2s.',
metric: '+45%',
metricLabel: 'Conversion Rate',
tag: 'Web',
tagColor: 'bg-primary-100 text-primary-700',
icon: 'web',
gradient: 'from-primary-500/10 to-primary-600/5',
borderColor: 'border-primary-200',
},
{
service: 'Mobile Development',
client: 'Retail App',
result: '200K+ downloads in first month, 4.9★ App Store rating',
description: 'Built a cross-platform shopping app with AR try-on features using React Native, launching simultaneously on iOS and Android.',
metric: '200K+',
metricLabel: 'Downloads',
tag: 'Mobile',
tagColor: 'bg-accent-100 text-accent-700',
icon: 'mobile',
gradient: 'from-accent-500/10 to-accent-600/5',
borderColor: 'border-accent-200',
},
{
service: 'AI & ML',
client: 'Healthcare Provider',
result: '60% reduction in manual processing, 94% diagnostic accuracy',
description: 'Deployed a computer vision model for medical imaging analysis, reducing radiologist review time and improving early detection rates.',
metric: '60%',
metricLabel: 'Time Saved',
tag: 'AI/ML',
tagColor: 'bg-emerald-100 text-emerald-700',
icon: 'ai',
gradient: 'from-emerald-500/10 to-emerald-600/5',
borderColor: 'border-emerald-200',
},
];
// Process steps
const processSteps = [
{
number: '01',
title: 'Discovery',
description: 'We analyze your requirements, goals, and challenges to define the project scope.',
icon: 'search',
},
{
number: '02',
title: 'Strategy',
description: 'Our team develops a comprehensive roadmap tailored to your business objectives.',
icon: 'map',
},
{
number: '03',
title: 'Development',
description: 'We build your solution using agile methodologies with regular progress updates.',
icon: 'code',
},
{
number: '04',
title: 'Launch & Support',
description: 'We deploy, monitor, and continuously improve your solution post-launch.',
icon: 'rocket',
},
];
---
<BaseLayout
title="IT Services & Solutions"
description="Expert web development, mobile apps, AI/ML solutions & cloud services from WorkRoot IT Solutions. 100+ projects delivered, 50+ happy clients. From $2,500 — get a free quote today."
>
<SEO
slot="head"
type="Service"
breadcrumbs={[
{ name: 'Home', url: '/' },
{ name: 'Services', url: '/services' }
]}
service={{
name: 'IT Solutions & Software Development',
description: 'Full-stack software development services including web development, mobile apps, AI & machine learning, and cloud infrastructure. Serving startups and enterprises worldwide.'
}}
faq={[
{
question: 'What IT services does WorkRoot offer?',
answer: 'WorkRoot offers four core services: Web Development (React, Next.js, Node.js), Mobile App Development (iOS, Android, React Native, Flutter), AI & Machine Learning (Python, TensorFlow, LangChain), and Cloud Services (AWS, Azure, GCP, Kubernetes).'
},
{
question: 'How much does a web development project cost?',
answer: 'Our pricing starts at $2,500 for Starter projects (up to 5 pages), $8,500 for Growth projects with custom features and integrations, and custom quotes for Enterprise solutions. Contact us for a free estimate tailored to your requirements.'
},
{
question: 'How long does software development take?',
answer: 'Project timelines depend on scope. A simple website takes 46 weeks, a complex web app 36 months, and an enterprise solution can take 612 months. We provide detailed timelines during the discovery phase.'
},
{
question: 'Do you offer post-launch support and maintenance?',
answer: 'Yes. All packages include post-launch support: 1 month for Starter, 3 months for Growth, and 12 months for Enterprise. We also offer ongoing SLA-backed maintenance retainers for continued peace of mind.'
},
{
question: 'Can WorkRoot build AI-powered features into my application?',
answer: 'Absolutely. Our AI/ML team integrates natural language processing, computer vision, recommendation engines, and predictive analytics into web and mobile products. We have delivered 25+ AI models with 94% average accuracy.'
}
]}
/>
<!-- Hero Section -->
<section class="relative bg-gradient-to-br from-secondary-900 via-secondary-800 to-secondary-900 py-24 lg:py-32 overflow-hidden">
<div class="absolute inset-0 overflow-hidden pointer-events-none" aria-hidden="true">
<div class="absolute top-1/4 left-0 w-80 h-80 bg-primary-500/10 rounded-full blur-3xl animate-pulse-slower"></div>
<div class="absolute bottom-0 right-1/4 w-96 h-96 bg-accent-500/8 rounded-full blur-3xl animate-pulse-slow"></div>
<div class="absolute top-0 right-0 w-64 h-64 bg-emerald-500/8 rounded-full blur-3xl"></div>
<!-- Grid pattern overlay -->
<div class="absolute inset-0 opacity-5" style="background-image: linear-gradient(rgba(255,255,255,.05) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,.05) 1px, transparent 1px); background-size: 60px 60px;"></div>
</div>
<div class="container-wrapper relative">
<div class="max-w-3xl mx-auto text-center reveal-on-scroll">
<span class="inline-flex items-center gap-2 px-4 py-1.5 mb-6 text-sm font-semibold text-primary-300 bg-primary-500/10 rounded-full border border-primary-500/20">
<span class="w-1.5 h-1.5 rounded-full bg-primary-400 animate-pulse"></span>
Our Services
</span>
<h1 class="text-4xl sm:text-5xl lg:text-6xl font-bold text-white mb-6 leading-tight">
Solutions That
<span class="text-transparent bg-clip-text bg-gradient-to-r from-primary-400 via-primary-300 to-accent-400">
Power Growth
</span>
</h1>
<p class="text-lg sm:text-xl text-secondary-300 max-w-2xl mx-auto mb-10 leading-relaxed">
From concept to deployment, we deliver end-to-end technology solutions that transform businesses and drive measurable results.
</p>
<!-- Quick service nav pills -->
<div class="flex flex-wrap gap-2 justify-center">
{services.map((service) => (
<a
href={`#${service.id}`}
class="px-4 py-1.5 text-sm font-medium text-white/70 hover:text-white bg-white/5 hover:bg-white/10 rounded-full border border-white/10 hover:border-white/20 transition-all duration-200"
>
{service.title}
</a>
))}
</div>
</div>
<!-- Floating stats bar -->
<div class="mt-16 grid grid-cols-2 sm:grid-cols-4 gap-4 lg:gap-6 reveal-on-scroll reveal-delay-2">
{[
{ value: '100+', label: 'Projects Delivered' },
{ value: '50+', label: 'Happy Clients' },
{ value: '4', label: 'Core Services' },
{ value: '5★', label: 'Average Rating' },
].map((stat) => (
<div class="bg-white/5 backdrop-blur-sm rounded-xl p-4 lg:p-5 text-center border border-white/10">
<p class="text-2xl lg:text-3xl font-bold text-white">{stat.value}</p>
<p class="text-secondary-400 text-sm mt-1">{stat.label}</p>
</div>
))}
</div>
</div>
</section>
<!-- Services Cards Section -->
<section class="py-16 lg:py-24 bg-white">
<div class="container-wrapper">
<div class="text-center max-w-2xl mx-auto mb-12 lg:mb-16 reveal-on-scroll">
<span class="text-primary-500 font-semibold text-sm uppercase tracking-wider">What We Offer</span>
<h2 class="text-3xl sm:text-4xl font-bold text-secondary-900 mt-2 mb-4">Our Core Services</h2>
<p class="text-secondary-600 text-lg">
Comprehensive technology solutions designed to accelerate your digital transformation journey.
</p>
</div>
<div class="grid sm:grid-cols-2 gap-6 lg:gap-8">
{services.map((service, idx) => (
<a
href={`#${service.id}`}
class={`service-card group relative bg-white rounded-2xl p-8 shadow-sm border border-secondary-100 hover:shadow-2xl hover:border-transparent hover:-translate-y-2 transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-primary-400 focus:ring-offset-2 reveal-on-scroll reveal-delay-${idx + 1}`}
aria-label={`Learn more about ${service.title}`}
>
<!-- Hover gradient overlay -->
<div class={`absolute inset-0 bg-gradient-to-br ${service.gradient} rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-300`} aria-hidden="true"></div>
<!-- Top border accent -->
<div class={`absolute top-0 left-8 right-8 h-0.5 bg-gradient-to-r ${service.gradient} rounded-full opacity-0 group-hover:opacity-100 transition-opacity duration-300`} aria-hidden="true"></div>
<div class="relative">
<!-- Icon + stats row -->
<div class="flex items-start justify-between mb-6">
<div class={`w-14 h-14 ${service.bgLight} ${service.iconColor} rounded-xl flex items-center justify-center group-hover:bg-white/20 group-hover:text-white transition-all duration-300 flex-shrink-0`}>
{service.icon === 'web' && (
<svg xmlns="http://www.w3.org/2000/svg" class="w-7 h-7" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
</svg>
)}
{service.icon === 'mobile' && (
<svg xmlns="http://www.w3.org/2000/svg" class="w-7 h-7" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
)}
{service.icon === 'ai' && (
<svg xmlns="http://www.w3.org/2000/svg" class="w-7 h-7" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
)}
{service.icon === 'cloud' && (
<svg xmlns="http://www.w3.org/2000/svg" class="w-7 h-7" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
</svg>
)}
</div>
<!-- Mini stats -->
<div class="text-right">
{Object.entries(service.stats).slice(0, 1).map(([key, val]) => (
<div>
<p class="text-xl font-bold text-secondary-900 group-hover:text-white transition-colors duration-300">{val}</p>
<p class="text-xs text-secondary-500 group-hover:text-white/70 transition-colors duration-300 capitalize">{key.replace(/([A-Z])/g, ' $1').trim()}</p>
</div>
))}
</div>
</div>
<!-- Content -->
<h3 class="text-xl font-bold text-secondary-900 group-hover:text-white mb-2 transition-colors duration-300">
{service.title}
</h3>
<p class="text-secondary-600 group-hover:text-white/80 mb-5 transition-colors duration-300 text-sm leading-relaxed">
{service.shortDescription}
</p>
<!-- Tech stack tags -->
<div class="flex flex-wrap gap-1.5 mb-5">
{service.techStack.map((tech) => (
<span class={`px-2.5 py-1 text-xs font-medium ${service.tagBg} group-hover:bg-white/20 group-hover:text-white rounded-md transition-colors duration-300`}>
{tech}
</span>
))}
</div>
<!-- Learn more link -->
<span class="inline-flex items-center text-primary-500 group-hover:text-white font-semibold text-sm transition-colors duration-300">
Explore Service
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 ml-1.5 group-hover:translate-x-1.5 transition-transform duration-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</span>
</div>
</a>
))}
</div>
</div>
</section>
<!-- Service Details Sections -->
{services.map((service, index) => (
<section
id={service.id}
class={`py-16 lg:py-24 ${index % 2 === 0 ? 'bg-secondary-50' : 'bg-white'}`}
>
<div class="container-wrapper">
<div class="grid lg:grid-cols-2 gap-12 lg:gap-16 items-center">
<!-- Content column -->
<div class={`reveal-on-scroll ${index % 2 === 1 ? 'lg:order-2' : ''}`}>
<div class={`inline-flex items-center gap-2 px-4 py-2 ${service.bgLight} ${service.iconColor} rounded-full text-sm font-semibold mb-4`}>
{service.icon === 'web' && (
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
</svg>
)}
{service.icon === 'mobile' && (
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
)}
{service.icon === 'ai' && (
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
)}
{service.icon === 'cloud' && (
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
</svg>
)}
{service.title}
</div>
<h2 class="text-3xl sm:text-4xl font-bold text-secondary-900 mb-4">
{service.title}
</h2>
<p class="text-secondary-600 text-lg leading-relaxed mb-6">
{service.fullDescription}
</p>
<!-- Service stats row -->
<div class="flex flex-wrap gap-4 mb-8">
{Object.entries(service.stats).map(([key, val]) => (
<div class={`flex flex-col px-4 py-3 ${service.bgLight} rounded-lg`}>
<span class={`text-xl font-bold ${service.iconColor}`}>{val}</span>
<span class="text-secondary-500 text-xs capitalize">{key.replace(/([A-Z])/g, ' $1').trim()}</span>
</div>
))}
</div>
<!-- Features list -->
<ul class="space-y-2.5 mb-8">
{service.features.map((feature) => (
<li class="flex items-start gap-3">
<span class={`w-5 h-5 ${service.iconColor} mt-0.5 flex-shrink-0`} aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
</span>
<span class="text-secondary-700">{feature}</span>
</li>
))}
</ul>
<!-- Tech stack -->
<div class="mb-8">
<p class="text-xs font-semibold text-secondary-400 uppercase tracking-wider mb-2">Technologies</p>
<div class="flex flex-wrap gap-2">
{service.techStack.map((tech) => (
<span class={`px-3 py-1 text-sm font-medium ${service.tagBg} rounded-md`}>{tech}</span>
))}
</div>
</div>
<a
href="/contact"
class={`inline-flex items-center justify-center px-7 py-3.5 bg-gradient-to-r ${service.gradient} text-white font-semibold rounded-lg hover:shadow-lg hover:opacity-95 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-primary-400 focus:ring-offset-2`}
>
Get Started
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 ml-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</a>
</div>
<!-- Visual element -->
<div class={`relative reveal-on-scroll reveal-delay-2 ${index % 2 === 1 ? 'lg:order-1' : ''}`}>
<div class={`aspect-square rounded-2xl bg-gradient-to-br ${service.gradient} p-8 lg:p-12 flex items-center justify-center`}>
<div class="relative w-full h-full bg-white/10 rounded-xl backdrop-blur-sm p-6 lg:p-8">
<!-- Window chrome decoration -->
<div class="absolute top-4 left-4 flex gap-1.5" aria-hidden="true">
<div class="w-3 h-3 rounded-full bg-red-400/60"></div>
<div class="w-3 h-3 rounded-full bg-yellow-400/60"></div>
<div class="w-3 h-3 rounded-full bg-green-400/60"></div>
</div>
<!-- Service-specific visual illustration -->
<div class="h-full flex flex-col justify-center items-center text-white pt-6">
{service.icon === 'web' && (
<div class="w-full space-y-3">
<div class="h-3 bg-white/30 rounded-full w-3/4 mx-auto"></div>
<div class="h-3 bg-white/20 rounded-full w-full"></div>
<div class="h-3 bg-white/20 rounded-full w-5/6 mx-auto"></div>
<div class="mt-4 grid grid-cols-3 gap-2">
<div class="h-16 bg-white/20 rounded-lg"></div>
<div class="h-16 bg-white/20 rounded-lg"></div>
<div class="h-16 bg-white/20 rounded-lg"></div>
</div>
<div class="h-2 bg-white/15 rounded-full w-1/2 mx-auto mt-2"></div>
</div>
)}
{service.icon === 'mobile' && (
<div class="flex gap-4 items-end justify-center w-full">
<div class="w-20 h-36 bg-white/20 rounded-2xl border-2 border-white/30 flex flex-col items-center pt-3 gap-2">
<div class="w-6 h-1 bg-white/40 rounded-full"></div>
<div class="w-12 h-2 bg-white/25 rounded-full"></div>
<div class="w-10 h-2 bg-white/20 rounded-full"></div>
<div class="w-12 h-8 bg-white/15 rounded-lg mt-1"></div>
</div>
<div class="w-24 h-44 bg-white/25 rounded-2xl border-2 border-white/40 flex flex-col items-center pt-3 gap-2">
<div class="w-8 h-1 bg-white/50 rounded-full"></div>
<div class="w-16 h-2 bg-white/30 rounded-full"></div>
<div class="w-14 h-2 bg-white/25 rounded-full"></div>
<div class="w-16 h-10 bg-white/20 rounded-lg mt-1"></div>
<div class="w-12 h-2 bg-white/20 rounded-full"></div>
</div>
<div class="w-20 h-36 bg-white/20 rounded-2xl border-2 border-white/30 flex flex-col items-center pt-3 gap-2">
<div class="w-6 h-1 bg-white/40 rounded-full"></div>
<div class="w-12 h-2 bg-white/25 rounded-full"></div>
<div class="w-10 h-2 bg-white/20 rounded-full"></div>
<div class="w-12 h-8 bg-white/15 rounded-lg mt-1"></div>
</div>
</div>
)}
{service.icon === 'ai' && (
<div class="w-full space-y-3 px-2">
<div class="flex items-center gap-2">
<div class="w-8 h-8 rounded-full bg-white/30 flex-shrink-0"></div>
<div class="flex-1 h-8 bg-white/20 rounded-lg"></div>
</div>
<div class="flex items-center gap-2 flex-row-reverse">
<div class="w-8 h-8 rounded-full bg-white/20 flex-shrink-0"></div>
<div class="flex-1 h-10 bg-white/15 rounded-lg"></div>
</div>
<div class="flex items-center gap-2">
<div class="w-8 h-8 rounded-full bg-white/30 flex-shrink-0"></div>
<div class="flex-1 h-8 bg-white/20 rounded-lg"></div>
</div>
<div class="flex items-center gap-2 flex-row-reverse">
<div class="w-8 h-8 rounded-full bg-white/20 flex-shrink-0"></div>
<div class="w-2/3 h-8 bg-white/15 rounded-lg"></div>
</div>
</div>
)}
{service.icon === 'cloud' && (
<div class="w-full space-y-3">
<div class="flex justify-center">
<div class="w-24 h-12 bg-white/30 rounded-2xl flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" class="w-8 h-8 text-white/70" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
</svg>
</div>
</div>
<div class="flex justify-center gap-1">
<div class="w-0.5 h-6 bg-white/30"></div>
<div class="w-0.5 h-6 bg-white/30 mx-6"></div>
<div class="w-0.5 h-6 bg-white/30 mx-6"></div>
</div>
<div class="grid grid-cols-3 gap-2">
{['AWS', 'K8s', 'GCP'].map((label) => (
<div class="h-10 bg-white/20 rounded-lg flex items-center justify-center">
<span class="text-white/70 text-xs font-medium">{label}</span>
</div>
))}
</div>
</div>
)}
</div>
</div>
</div>
<!-- Floating stats badge -->
<div class="absolute -bottom-4 -right-4 sm:-bottom-6 sm:-right-6 bg-white rounded-xl shadow-xl p-4 sm:p-5 hidden sm:block border border-secondary-100">
<div class="flex items-center gap-3">
<div class={`w-10 h-10 ${service.bgLight} ${service.iconColor} rounded-lg flex items-center justify-center flex-shrink-0`} aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" />
</svg>
</div>
<div>
<p class="text-lg font-bold text-secondary-900">{Object.values(service.stats)[0]}</p>
<p class="text-secondary-500 text-xs">{Object.keys(service.stats)[0].replace(/([A-Z])/g, ' $1').trim()}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
))}
<!-- Case Studies Section -->
<section class="py-16 lg:py-24 bg-secondary-50">
<div class="container-wrapper">
<div class="text-center max-w-2xl mx-auto mb-12 lg:mb-16 reveal-on-scroll">
<span class="text-accent-500 font-semibold text-sm uppercase tracking-wider">Real Results</span>
<h2 class="text-3xl sm:text-4xl font-bold text-secondary-900 mt-2 mb-4">Client Success Stories</h2>
<p class="text-secondary-600 text-lg">
Don't take our word for it — see the measurable impact we've delivered for our clients.
</p>
</div>
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-6 lg:gap-8">
{caseStudies.map((study, idx) => (
<div class={`case-study-card group relative bg-white rounded-2xl p-6 lg:p-8 border ${study.borderColor} hover:shadow-xl hover:-translate-y-1 transition-all duration-300 overflow-hidden reveal-on-scroll reveal-delay-${idx + 1}`}>
<!-- Background gradient -->
<div class={`absolute inset-0 bg-gradient-to-br ${study.gradient} opacity-50 group-hover:opacity-70 transition-opacity duration-300`} aria-hidden="true"></div>
<div class="relative">
<!-- Tag + icon -->
<div class="flex items-center justify-between mb-4">
<span class={`px-2.5 py-1 text-xs font-semibold ${study.tagColor} rounded-md`}>{study.tag}</span>
<div class="text-secondary-300" aria-hidden="true">
{study.icon === 'web' && (
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
</svg>
)}
{study.icon === 'mobile' && (
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
)}
{study.icon === 'ai' && (
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
)}
</div>
</div>
<!-- Big metric -->
<p class="text-4xl font-bold text-secondary-900 mb-1">{study.metric}</p>
<p class="text-sm font-medium text-secondary-500 mb-4">{study.metricLabel}</p>
<!-- Client & description -->
<p class="font-semibold text-secondary-800 mb-2">{study.client}</p>
<p class="text-secondary-600 text-sm leading-relaxed mb-4">{study.description}</p>
<!-- Result pill -->
<div class="inline-flex items-center gap-1.5 px-3 py-1 bg-secondary-100 rounded-full text-xs text-secondary-600 font-medium">
<svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5 text-emerald-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
{study.result}
</div>
</div>
</div>
))}
</div>
<div class="mt-10 text-center">
<a
href="/contact"
class="inline-flex items-center gap-2 text-primary-600 hover:text-primary-700 font-semibold text-sm transition-colors duration-200 group"
>
Start your success story
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 group-hover:translate-x-1 transition-transform duration-200" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</a>
</div>
</div>
</section>
<!-- Pricing Section -->
<section class="py-16 lg:py-24 bg-white">
<div class="container-wrapper">
<div class="text-center max-w-2xl mx-auto mb-12 lg:mb-16 reveal-on-scroll">
<span class="text-primary-500 font-semibold text-sm uppercase tracking-wider">Transparent Pricing</span>
<h2 class="text-3xl sm:text-4xl font-bold text-secondary-900 mt-2 mb-4">Investment Tiers</h2>
<p class="text-secondary-600 text-lg">
Straightforward pricing to fit businesses at every stage. All plans include dedicated project management.
</p>
</div>
<div class="grid md:grid-cols-3 gap-6 lg:gap-8 items-stretch">
{pricingTiers.map((tier, idx) => (
<div
class={`relative flex flex-col rounded-2xl p-7 lg:p-8 reveal-on-scroll reveal-delay-${idx + 1} transition-all duration-300 ${
tier.highlighted
? 'bg-secondary-900 shadow-2xl ring-2 ring-primary-500 md:scale-105'
: 'bg-white border border-secondary-200 hover:shadow-xl hover:-translate-y-1'
}`}
>
{tier.highlighted && (
<div class="absolute -top-3.5 left-1/2 -translate-x-1/2">
<span class="px-4 py-1 bg-primary-500 text-white text-xs font-bold rounded-full uppercase tracking-wide shadow-lg">
Most Popular
</span>
</div>
)}
<div class="mb-6">
<h3 class={`text-xl font-bold mb-1 ${tier.highlighted ? 'text-white' : 'text-secondary-900'}`}>
{tier.name}
</h3>
<p class={`text-sm mb-4 ${tier.highlighted ? 'text-secondary-400' : 'text-secondary-500'}`}>
{tier.description}
</p>
<div class="flex items-baseline gap-2">
<span class={`text-4xl font-bold ${tier.highlighted ? 'text-white' : 'text-secondary-900'}`}>
{tier.price}
</span>
<span class={`text-sm ${tier.highlighted ? 'text-secondary-400' : 'text-secondary-500'}`}>
{tier.period}
</span>
</div>
</div>
<ul class="space-y-3 mb-8 flex-1">
{tier.features.map((feature) => (
<li class="flex items-start gap-3">
<svg
xmlns="http://www.w3.org/2000/svg"
class={`w-5 h-5 flex-shrink-0 mt-0.5 ${tier.highlighted ? 'text-primary-400' : 'text-primary-500'}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2.5"
aria-hidden="true"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
<span class={`text-sm ${tier.highlighted ? 'text-secondary-300' : 'text-secondary-700'}`}>
{feature}
</span>
</li>
))}
</ul>
<a
href="/contact"
class={`w-full inline-flex items-center justify-center px-6 py-3.5 rounded-lg font-semibold transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 ${
tier.highlighted
? 'bg-primary-500 text-white hover:bg-primary-400 focus:ring-primary-400 focus:ring-offset-secondary-900'
: 'bg-secondary-900 text-white hover:bg-secondary-800 focus:ring-secondary-400'
}`}
>
{tier.cta === 'Most Popular' ? 'Get Started' : tier.cta}
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 ml-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</a>
</div>
))}
</div>
<p class="text-center text-secondary-500 text-sm mt-8">
All prices are estimates. Final scope and pricing determined after discovery call.
<a href="/contact" class="text-primary-600 hover:text-primary-700 font-medium ml-1">Schedule a free consultation →</a>
</p>
</div>
</section>
<!-- Process Section -->
<section class="py-16 lg:py-24 bg-gradient-to-br from-secondary-900 via-secondary-800 to-secondary-900">
<div class="container-wrapper">
<div class="text-center max-w-2xl mx-auto mb-12 lg:mb-16 reveal-on-scroll">
<span class="text-primary-400 font-semibold text-sm uppercase tracking-wider">How We Work</span>
<h2 class="text-3xl sm:text-4xl font-bold text-white mt-2 mb-4">Our Proven Process</h2>
<p class="text-secondary-300 text-lg">
A battle-tested methodology that delivers consistent, high-quality results — on time, every time.
</p>
</div>
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-4">
{processSteps.map((step, index) => (
<div class={`relative reveal-on-scroll reveal-delay-${index + 1}`}>
<div class="group bg-white/5 backdrop-blur-sm rounded-2xl p-6 lg:p-7 border border-white/10 hover:bg-white/10 hover:border-white/20 hover:-translate-y-1 transition-all duration-300 h-full">
<!-- Step number with glow -->
<div class="relative mb-4">
<span class="text-6xl font-bold text-primary-500/20 leading-none">{step.number}</span>
<div class="absolute -top-1 -left-1 w-8 h-8 bg-primary-500/10 rounded-full blur-md"></div>
</div>
<h3 class="text-lg font-bold text-white mb-2">{step.title}</h3>
<p class="text-secondary-400 text-sm leading-relaxed">{step.description}</p>
</div>
{/* Connector arrow between steps */}
{index < processSteps.length - 1 && (
<div class="hidden lg:flex absolute top-1/2 -right-5 transform -translate-y-1/2 z-10 items-center justify-center w-10">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-primary-500/40" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
</svg>
</div>
)}
</div>
))}
</div>
</div>
</section>
<!-- CTA Section -->
<section class="py-16 lg:py-24 bg-gradient-to-r from-primary-600 via-primary-500 to-primary-600 relative overflow-hidden">
<div class="absolute inset-0 pointer-events-none" aria-hidden="true">
<div class="absolute top-0 left-1/4 w-64 h-64 bg-white/5 rounded-full blur-3xl"></div>
<div class="absolute bottom-0 right-1/4 w-80 h-80 bg-white/5 rounded-full blur-3xl"></div>
</div>
<div class="container-wrapper text-center relative reveal-on-scroll">
<span class="inline-block px-4 py-1.5 mb-4 text-sm font-medium text-white/80 bg-white/10 rounded-full border border-white/20">
Ready to get started?
</span>
<h2 class="text-3xl sm:text-4xl lg:text-5xl font-bold text-white mb-4">
Let's Build Something
<span class="italic font-normal"> Great </span>
Together
</h2>
<p class="text-primary-100 text-lg max-w-2xl mx-auto mb-10 leading-relaxed">
Schedule a free 30-minute discovery call and let's discuss how our services can accelerate your business goals.
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a
href="/contact"
class="inline-flex items-center justify-center px-8 py-4 bg-white text-primary-600 font-bold rounded-xl hover:bg-primary-50 hover:shadow-xl transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-primary-600"
>
Schedule a Free Call
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 ml-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</a>
<a
href="/about"
class="inline-flex items-center justify-center px-8 py-4 border-2 border-white/40 text-white font-semibold rounded-xl hover:bg-white/10 hover:border-white/60 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-primary-600"
>
Learn About Us
</a>
</div>
<!-- Trust signals -->
<div class="mt-10 flex flex-wrap items-center justify-center gap-6 text-white/60 text-sm">
<span class="flex items-center gap-1.5">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
No commitment required
</span>
<span class="flex items-center gap-1.5">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
Free project estimate
</span>
<span class="flex items-center gap-1.5">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
Response within 24 hours
</span>
</div>
</div>
</section>
</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;
}
}
/* Service card inner border accent animation */
.service-card::before {
content: '';
position: absolute;
inset: 0;
border-radius: 1rem;
padding: 1.5px;
background: linear-gradient(135deg, transparent 0%, transparent 50%);
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
pointer-events: none;
transition: background 0.3s ease;
}
/* Case study card hover */
.case-study-card {
contain: layout style;
}
</style>
<script>
// Intersection Observer for scroll-reveal animations
(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));
})();
</script>
+28
View File
@@ -0,0 +1,28 @@
import type { APIRoute } from 'astro';
const site = 'https://workroot.in';
/**
* Sitemap Index - Provides links to individual sitemaps
* Use this when you have multiple content types or large amounts of content
* Currently serves as documentation for future scalability
*/
export const GET: APIRoute = async () => {
const lastmod = new Date().toISOString();
const sitemapIndex = `<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<sitemap>
<loc>${site}/sitemap.xml</loc>
<lastmod>${lastmod}</lastmod>
</sitemap>
</sitemapindex>`;
return new Response(sitemapIndex, {
headers: {
'Content-Type': 'application/xml; charset=utf-8',
'Cache-Control': 'public, max-age=3600',
'X-Robots-Tag': 'noindex',
},
});
};
+159
View File
@@ -0,0 +1,159 @@
---
// Sitemap Page for WorkRoot IT Solutions
// Features: Visual sitemap with links to all pages organized by category
import BaseLayout from '../layouts/BaseLayout.astro';
// Sitemap structure
const sitemapSections = [
{
title: 'Main Pages',
icon: 'home',
links: [
{ name: 'Home', url: '/', description: 'Welcome to WorkRoot IT Solutions' },
{ name: 'About Us', url: '/about', description: 'Our story, team, and mission' },
{ name: 'Services', url: '/services', description: 'Our IT service offerings' },
{ name: 'Portfolio', url: '/portfolio', description: 'Featured projects and case studies' },
{ name: 'Blog', url: '/blog', description: 'Insights and articles' },
{ name: 'Contact', url: '/contact', description: 'Get in touch with us' },
],
},
{
title: 'Services',
icon: 'briefcase',
links: [
{ name: 'Web Development', url: '/services#web-development', description: 'Custom web applications and e-commerce' },
{ name: 'Mobile Development', url: '/services#mobile-development', description: 'iOS, Android, and cross-platform apps' },
{ name: 'AI & Machine Learning', url: '/services#ai-ml', description: 'Intelligent solutions and automation' },
{ name: 'Cloud Services', url: '/services#cloud', description: 'Cloud migration and infrastructure' },
],
},
{
title: 'Resources',
icon: 'book',
links: [
{ name: 'Blog', url: '/blog', description: 'Latest articles and insights' },
{ name: 'Case Studies', url: '/portfolio', description: 'Detailed project analyses' },
],
},
{
title: 'Company',
icon: 'building',
links: [
{ name: 'About Us', url: '/about', description: 'Learn about our company' },
{ name: 'Our Team', url: '/about#team', description: 'Meet our experts' },
{ name: 'Careers', url: '/contact', description: 'Join our team' },
],
},
{
title: 'Legal',
icon: 'shield',
links: [
{ name: 'Privacy Policy', url: '/privacy', description: 'How we handle your data' },
{ name: 'Terms of Service', url: '/terms', description: 'Terms and conditions' },
{ name: 'Sitemap', url: '/sitemap', description: 'This page' },
],
},
{
title: 'Connect',
icon: 'share',
links: [
{ name: 'Contact Us', url: '/contact', description: 'Send us a message' },
{ name: 'LinkedIn', url: '#', description: 'Follow us on LinkedIn', external: true },
{ name: 'Twitter', url: '#', description: 'Follow us on Twitter', external: true },
{ name: 'GitHub', url: '#', description: 'View our open source projects', external: true },
],
},
];
// Icons mapping
const icons: Record<string, string> = {
home: `<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path></svg>`,
briefcase: `<svg class="w-6 h-6" 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>`,
book: `<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path></svg>`,
building: `<svg class="w-6 h-6" 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>`,
shield: `<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><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"></path></svg>`,
share: `<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"></path></svg>`,
};
---
<BaseLayout
title="Sitemap"
description="WorkRoot IT Solutions Sitemap - Navigate to all pages and resources on our website."
>
<!-- Hero Section -->
<section class="relative bg-gradient-to-br from-secondary-900 via-secondary-800 to-secondary-900 py-16 lg:py-20">
<div class="absolute inset-0 overflow-hidden">
<div class="absolute top-0 left-1/4 w-96 h-96 bg-primary-500/10 rounded-full blur-3xl"></div>
<div class="absolute bottom-0 right-1/4 w-96 h-96 bg-accent-500/10 rounded-full blur-3xl"></div>
</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">
<h1 class="text-4xl md:text-5xl font-bold text-white mb-4">
Site<span class="text-transparent bg-clip-text bg-gradient-to-r from-primary-400 to-accent-400">map</span>
</h1>
<p class="text-lg text-secondary-300">
Find your way around our website
</p>
</div>
</div>
</section>
<!-- Sitemap Grid Section -->
<section class="py-16 lg:py-24 bg-secondary-950">
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{sitemapSections.map((section) => (
<div class="bg-secondary-900/50 border border-secondary-800 rounded-2xl p-6 hover:border-primary-500/30 transition-colors duration-300">
<div class="flex items-center gap-3 mb-6">
<div class="flex items-center justify-center w-12 h-12 rounded-xl bg-primary-500/20 text-primary-400" set:html={icons[section.icon]} />
<h2 class="text-xl font-bold text-white">{section.title}</h2>
</div>
<ul class="space-y-3">
{section.links.map((link) => (
<li>
<a
href={link.url}
class="group flex items-start gap-3 p-3 rounded-lg hover:bg-secondary-800/50 transition-colors duration-200"
target={link.external ? '_blank' : undefined}
rel={link.external ? 'noopener noreferrer' : undefined}
>
<svg class="w-5 h-5 mt-0.5 text-primary-400 group-hover:translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
<div>
<span class="text-white font-medium group-hover:text-primary-400 transition-colors duration-200">
{link.name}
{link.external && (
<svg class="w-4 h-4 inline-block ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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"></path>
</svg>
)}
</span>
<p class="text-sm text-secondary-400 mt-0.5">{link.description}</p>
</div>
</a>
</li>
))}
</ul>
</div>
))}
</div>
<!-- XML Sitemap Note -->
<div class="mt-16 text-center">
<div class="inline-flex items-center gap-3 bg-secondary-900/50 border border-secondary-800 rounded-xl px-6 py-4">
<svg class="w-6 h-6 text-primary-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<span class="text-secondary-300">
Looking for the XML sitemap? Visit
<a href="/sitemap.xml" class="text-primary-400 hover:text-primary-300 underline underline-offset-2">
/sitemap.xml
</a>
</span>
</div>
</div>
</div>
</section>
</BaseLayout>
+83
View File
@@ -0,0 +1,83 @@
import type { APIRoute } from 'astro';
import { getCollection } from 'astro:content';
const site = 'https://workroot.in';
interface SitemapEntry {
url: string;
lastmod?: string;
changefreq: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';
priority: number;
}
export const GET: APIRoute = async () => {
// Fetch blog posts (exclude drafts)
const posts = await getCollection('blog', ({ data }) => !data.draft);
// Fetch portfolio items if collection exists
let portfolioItems: any[] = [];
try {
portfolioItems = await getCollection('portfolio');
} catch (e) {
// Portfolio collection doesn't exist yet
}
const staticPages: SitemapEntry[] = [
{ url: '/', changefreq: 'weekly', priority: 1.0 },
{ url: '/services/', changefreq: 'monthly', priority: 0.9 },
{ url: '/portfolio/', changefreq: 'weekly', priority: 0.8 },
{ url: '/about/', changefreq: 'monthly', priority: 0.8 },
{ url: '/blog/', changefreq: 'weekly', priority: 0.8 },
{ url: '/contact/', changefreq: 'monthly', priority: 0.7 },
{ url: '/privacy/', changefreq: 'yearly', priority: 0.3 },
{ url: '/terms/', changefreq: 'yearly', priority: 0.3 },
{ url: '/sitemap/', changefreq: 'monthly', priority: 0.3 },
];
const blogPages: SitemapEntry[] = posts.map((post) => ({
url: `/blog/${post.slug}/`,
lastmod: post.data.updatedDate
? post.data.updatedDate.toISOString().split('T')[0]
: post.data.pubDate.toISOString().split('T')[0],
changefreq: 'monthly' as const,
priority: 0.7,
}));
const portfolioPages: SitemapEntry[] = portfolioItems.map((item) => ({
url: `/portfolio/${item.slug}/`,
lastmod: item.data.updatedDate
? item.data.updatedDate.toISOString().split('T')[0]
: item.data.date?.toISOString().split('T')[0],
changefreq: 'monthly' as const,
priority: 0.7,
}));
const allPages = [...staticPages, ...blogPages, ...portfolioPages];
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml"
xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
${allPages
.map(
(page) => ` <url>
<loc>${site}${page.url}</loc>${page.lastmod ? `
<lastmod>${page.lastmod}</lastmod>` : `
<lastmod>${new Date().toISOString().split('T')[0]}</lastmod>`}
<changefreq>${page.changefreq}</changefreq>
<priority>${page.priority}</priority>
</url>`
)
.join('\n')}
</urlset>`;
return new Response(sitemap, {
headers: {
'Content-Type': 'application/xml; charset=utf-8',
'Cache-Control': 'public, max-age=3600', // Cache for 1 hour
'X-Robots-Tag': 'noindex', // Don't index the sitemap itself
},
});
};
+310
View File
@@ -0,0 +1,310 @@
---
// Terms of Service Page for WorkRoot IT Solutions
// Features: Legal content with proper formatting for readability
import BaseLayout from '../layouts/BaseLayout.astro';
const lastUpdated = 'March 20, 2026';
---
<BaseLayout
title="Terms of Service"
description="WorkRoot IT Solutions Terms of Service - Read our terms and conditions for using our website and services."
noIndex={true}
>
<!-- Hero Section -->
<section class="relative bg-gradient-to-br from-secondary-900 via-secondary-800 to-secondary-900 py-16 lg:py-20">
<div class="absolute inset-0 overflow-hidden">
<div class="absolute top-0 left-1/4 w-96 h-96 bg-primary-500/10 rounded-full blur-3xl"></div>
<div class="absolute bottom-0 right-1/4 w-96 h-96 bg-accent-500/10 rounded-full blur-3xl"></div>
</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">
<h1 class="text-4xl md:text-5xl font-bold text-white mb-4">
Terms of <span class="text-transparent bg-clip-text bg-gradient-to-r from-primary-400 to-accent-400">Service</span>
</h1>
<p class="text-lg text-secondary-300">
Last updated: {lastUpdated}
</p>
</div>
</div>
</section>
<!-- Content Section -->
<section class="py-16 lg:py-24 bg-secondary-950">
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="max-w-4xl mx-auto">
<div class="prose prose-lg prose-invert prose-headings:text-white prose-p:text-secondary-300 prose-li:text-secondary-300 prose-strong:text-white max-w-none">
<!-- Introduction -->
<div class="bg-secondary-900/50 border border-secondary-800 rounded-2xl p-8 mb-8">
<p class="text-secondary-300 leading-relaxed">
Welcome to WorkRoot IT Solutions. These Terms of Service ("Terms") govern your access to and use of our website, products, and services. By accessing or using our services, you agree to be bound by these Terms. If you do not agree to these Terms, please do not use our services.
</p>
</div>
<!-- Section 1 -->
<div class="mb-12">
<h2 class="text-2xl font-bold text-white mb-4 flex items-center gap-3">
<span class="flex items-center justify-center w-10 h-10 rounded-full bg-primary-500/20 text-primary-400 text-lg font-bold">1</span>
Acceptance of Terms
</h2>
<div class="pl-13">
<p class="text-secondary-300 mb-4">By accessing or using our services, you represent that:</p>
<ul class="list-disc pl-6 space-y-2 text-secondary-300">
<li>You are at least 18 years of age or the age of majority in your jurisdiction</li>
<li>You have the legal capacity to enter into a binding agreement</li>
<li>If accepting on behalf of an organization, you have authority to bind that organization</li>
<li>You will comply with all applicable laws and regulations</li>
</ul>
</div>
</div>
<!-- Section 2 -->
<div class="mb-12">
<h2 class="text-2xl font-bold text-white mb-4 flex items-center gap-3">
<span class="flex items-center justify-center w-10 h-10 rounded-full bg-primary-500/20 text-primary-400 text-lg font-bold">2</span>
Description of Services
</h2>
<div class="pl-13">
<p class="text-secondary-300 mb-4">WorkRoot IT Solutions provides a range of IT services, including but not limited to:</p>
<ul class="list-disc pl-6 space-y-2 text-secondary-300">
<li>Custom web and mobile application development</li>
<li>Cloud computing and infrastructure services</li>
<li>AI and machine learning solutions</li>
<li>IT consulting and digital transformation</li>
<li>DevOps and continuous integration/deployment</li>
<li>Maintenance and support services</li>
</ul>
<p class="text-secondary-300 mt-4">Specific services are subject to separate agreements, statements of work, or service level agreements.</p>
</div>
</div>
<!-- Section 3 -->
<div class="mb-12">
<h2 class="text-2xl font-bold text-white mb-4 flex items-center gap-3">
<span class="flex items-center justify-center w-10 h-10 rounded-full bg-primary-500/20 text-primary-400 text-lg font-bold">3</span>
User Accounts
</h2>
<div class="pl-13">
<p class="text-secondary-300 mb-4">If you create an account with us, you agree to:</p>
<ul class="list-disc pl-6 space-y-2 text-secondary-300">
<li>Provide accurate, current, and complete information</li>
<li>Maintain and promptly update your account information</li>
<li>Keep your password secure and confidential</li>
<li>Accept responsibility for all activities under your account</li>
<li>Notify us immediately of any unauthorized access</li>
</ul>
<p class="text-secondary-300 mt-4">We reserve the right to suspend or terminate accounts that violate these Terms.</p>
</div>
</div>
<!-- Section 4 -->
<div class="mb-12">
<h2 class="text-2xl font-bold text-white mb-4 flex items-center gap-3">
<span class="flex items-center justify-center w-10 h-10 rounded-full bg-primary-500/20 text-primary-400 text-lg font-bold">4</span>
Intellectual Property Rights
</h2>
<div class="pl-13">
<h3 class="text-xl font-semibold text-white mt-6 mb-3">Our Content</h3>
<p class="text-secondary-300">All content on our website, including text, graphics, logos, images, software, and other materials, is the property of WorkRoot IT Solutions or its licensors and is protected by intellectual property laws. You may not copy, modify, distribute, or create derivative works without our prior written consent.</p>
<h3 class="text-xl font-semibold text-white mt-6 mb-3">Your Content</h3>
<p class="text-secondary-300">You retain ownership of content you provide to us. By submitting content, you grant us a non-exclusive, worldwide, royalty-free license to use, reproduce, and display such content in connection with providing our services.</p>
<h3 class="text-xl font-semibold text-white mt-6 mb-3">Deliverables</h3>
<p class="text-secondary-300">Ownership of custom deliverables (code, designs, documentation) is governed by the specific project agreement between you and WorkRoot IT Solutions.</p>
</div>
</div>
<!-- Section 5 -->
<div class="mb-12">
<h2 class="text-2xl font-bold text-white mb-4 flex items-center gap-3">
<span class="flex items-center justify-center w-10 h-10 rounded-full bg-primary-500/20 text-primary-400 text-lg font-bold">5</span>
Prohibited Conduct
</h2>
<div class="pl-13">
<p class="text-secondary-300 mb-4">You agree not to:</p>
<ul class="list-disc pl-6 space-y-2 text-secondary-300">
<li>Use our services for any illegal or unauthorized purpose</li>
<li>Violate any laws, regulations, or third-party rights</li>
<li>Transmit viruses, malware, or harmful code</li>
<li>Attempt to gain unauthorized access to our systems</li>
<li>Interfere with or disrupt our services or servers</li>
<li>Scrape, harvest, or collect data without permission</li>
<li>Impersonate any person or entity</li>
<li>Use our services to send spam or unsolicited messages</li>
<li>Reverse engineer or decompile our software</li>
</ul>
</div>
</div>
<!-- Section 6 -->
<div class="mb-12">
<h2 class="text-2xl font-bold text-white mb-4 flex items-center gap-3">
<span class="flex items-center justify-center w-10 h-10 rounded-full bg-primary-500/20 text-primary-400 text-lg font-bold">6</span>
Payment Terms
</h2>
<div class="pl-13">
<p class="text-secondary-300 mb-4">For paid services:</p>
<ul class="list-disc pl-6 space-y-2 text-secondary-300">
<li>Fees are specified in your service agreement or statement of work</li>
<li>Payment is due according to the agreed schedule</li>
<li>All fees are non-refundable unless otherwise stated</li>
<li>We reserve the right to suspend services for non-payment</li>
<li>You are responsible for all applicable taxes</li>
<li>Prices may be subject to change with reasonable notice</li>
</ul>
</div>
</div>
<!-- Section 7 -->
<div class="mb-12">
<h2 class="text-2xl font-bold text-white mb-4 flex items-center gap-3">
<span class="flex items-center justify-center w-10 h-10 rounded-full bg-primary-500/20 text-primary-400 text-lg font-bold">7</span>
Confidentiality
</h2>
<div class="pl-13">
<p class="text-secondary-300">Both parties agree to maintain the confidentiality of any proprietary or confidential information disclosed during the course of our engagement. This obligation survives the termination of these Terms. Confidential information does not include information that is publicly available, independently developed, or rightfully received from third parties.</p>
</div>
</div>
<!-- Section 8 -->
<div class="mb-12">
<h2 class="text-2xl font-bold text-white mb-4 flex items-center gap-3">
<span class="flex items-center justify-center w-10 h-10 rounded-full bg-primary-500/20 text-primary-400 text-lg font-bold">8</span>
Disclaimer of Warranties
</h2>
<div class="pl-13">
<div class="bg-accent-500/10 border border-accent-500/20 rounded-xl p-6 text-secondary-300">
<p class="mb-4">OUR SERVICES ARE PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO:</p>
<ul class="list-disc pl-6 space-y-2">
<li>MERCHANTABILITY</li>
<li>FITNESS FOR A PARTICULAR PURPOSE</li>
<li>NON-INFRINGEMENT</li>
<li>ACCURACY OR COMPLETENESS</li>
</ul>
<p class="mt-4">We do not warrant that our services will be uninterrupted, error-free, or secure.</p>
</div>
</div>
</div>
<!-- Section 9 -->
<div class="mb-12">
<h2 class="text-2xl font-bold text-white mb-4 flex items-center gap-3">
<span class="flex items-center justify-center w-10 h-10 rounded-full bg-primary-500/20 text-primary-400 text-lg font-bold">9</span>
Limitation of Liability
</h2>
<div class="pl-13">
<div class="bg-accent-500/10 border border-accent-500/20 rounded-xl p-6 text-secondary-300">
<p class="mb-4">TO THE MAXIMUM EXTENT PERMITTED BY LAW:</p>
<ul class="list-disc pl-6 space-y-2">
<li>WorkRoot IT Solutions shall not be liable for any indirect, incidental, special, consequential, or punitive damages</li>
<li>Our total liability shall not exceed the amounts paid by you in the twelve (12) months preceding the claim</li>
<li>We are not liable for damages arising from your use of third-party services or content</li>
</ul>
</div>
</div>
</div>
<!-- Section 10 -->
<div class="mb-12">
<h2 class="text-2xl font-bold text-white mb-4 flex items-center gap-3">
<span class="flex items-center justify-center w-10 h-10 rounded-full bg-primary-500/20 text-primary-400 text-lg font-bold">10</span>
Indemnification
</h2>
<div class="pl-13">
<p class="text-secondary-300">You agree to indemnify, defend, and hold harmless WorkRoot IT Solutions, its officers, directors, employees, and agents from any claims, damages, losses, or expenses (including reasonable attorney's fees) arising from:</p>
<ul class="list-disc pl-6 space-y-2 text-secondary-300 mt-4">
<li>Your use of our services</li>
<li>Your violation of these Terms</li>
<li>Your violation of any third-party rights</li>
<li>Your content or materials provided to us</li>
</ul>
</div>
</div>
<!-- Section 11 -->
<div class="mb-12">
<h2 class="text-2xl font-bold text-white mb-4 flex items-center gap-3">
<span class="flex items-center justify-center w-10 h-10 rounded-full bg-primary-500/20 text-primary-400 text-lg font-bold">11</span>
Termination
</h2>
<div class="pl-13">
<p class="text-secondary-300 mb-4">Either party may terminate these Terms:</p>
<ul class="list-disc pl-6 space-y-2 text-secondary-300">
<li>For convenience with thirty (30) days written notice</li>
<li>Immediately upon material breach by the other party</li>
<li>Immediately if required by law</li>
</ul>
<p class="text-secondary-300 mt-4">Upon termination, your right to use our services ceases. Sections relating to intellectual property, confidentiality, warranties, liability, and indemnification survive termination.</p>
</div>
</div>
<!-- Section 12 -->
<div class="mb-12">
<h2 class="text-2xl font-bold text-white mb-4 flex items-center gap-3">
<span class="flex items-center justify-center w-10 h-10 rounded-full bg-primary-500/20 text-primary-400 text-lg font-bold">12</span>
Dispute Resolution
</h2>
<div class="pl-13">
<p class="text-secondary-300 mb-4">Any disputes arising from these Terms shall be resolved as follows:</p>
<ul class="list-disc pl-6 space-y-2 text-secondary-300">
<li><strong class="text-white">Informal Resolution:</strong> The parties will first attempt to resolve disputes through good-faith negotiation</li>
<li><strong class="text-white">Mediation:</strong> If negotiation fails, disputes may be submitted to mediation</li>
<li><strong class="text-white">Arbitration:</strong> Binding arbitration under the rules of the American Arbitration Association</li>
</ul>
<p class="text-secondary-300 mt-4">These Terms shall be governed by the laws of the State of California, without regard to conflict of law principles.</p>
</div>
</div>
<!-- Section 13 -->
<div class="mb-12">
<h2 class="text-2xl font-bold text-white mb-4 flex items-center gap-3">
<span class="flex items-center justify-center w-10 h-10 rounded-full bg-primary-500/20 text-primary-400 text-lg font-bold">13</span>
Changes to Terms
</h2>
<div class="pl-13">
<p class="text-secondary-300">We reserve the right to modify these Terms at any time. We will provide notice of material changes by posting the updated Terms on our website and updating the "Last updated" date. Your continued use of our services after such changes constitutes acceptance of the modified Terms.</p>
</div>
</div>
<!-- Section 14 -->
<div class="mb-12">
<h2 class="text-2xl font-bold text-white mb-4 flex items-center gap-3">
<span class="flex items-center justify-center w-10 h-10 rounded-full bg-primary-500/20 text-primary-400 text-lg font-bold">14</span>
General Provisions
</h2>
<div class="pl-13">
<ul class="list-disc pl-6 space-y-2 text-secondary-300">
<li><strong class="text-white">Entire Agreement:</strong> These Terms constitute the entire agreement between you and WorkRoot IT Solutions</li>
<li><strong class="text-white">Severability:</strong> If any provision is found invalid, the remaining provisions remain in effect</li>
<li><strong class="text-white">Waiver:</strong> Failure to enforce any right does not constitute a waiver</li>
<li><strong class="text-white">Assignment:</strong> You may not assign these Terms without our written consent</li>
<li><strong class="text-white">Force Majeure:</strong> Neither party is liable for delays caused by circumstances beyond reasonable control</li>
</ul>
</div>
</div>
<!-- Section 15 -->
<div class="mb-12">
<h2 class="text-2xl font-bold text-white mb-4 flex items-center gap-3">
<span class="flex items-center justify-center w-10 h-10 rounded-full bg-primary-500/20 text-primary-400 text-lg font-bold">15</span>
Contact Information
</h2>
<div class="pl-13">
<p class="text-secondary-300 mb-4">For questions about these Terms, please contact us:</p>
<div class="bg-secondary-900/50 border border-secondary-800 rounded-xl p-6">
<p class="text-secondary-300"><strong class="text-white">WorkRoot IT Solutions</strong></p>
<p class="text-secondary-300">Email: legal@workroot.in</p>
<p class="text-secondary-300">Phone: +1 (555) 123-4567</p>
<p class="text-secondary-300">Address: 123 Tech Avenue, Suite 500, San Francisco, CA 94102</p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</BaseLayout>
+562
View File
@@ -0,0 +1,562 @@
/* Google Fonts are loaded non-blocking via <link rel="preload"> in BaseLayout.astro.
The @import is intentionally removed to avoid render-blocking font requests. */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* ============================================================
CSS CUSTOM PROPERTIES (Design Tokens)
Single source of truth for values used outside Tailwind
============================================================ */
:root {
/* Colors */
--color-primary: #0891b2;
--color-primary-dark: #0e7490;
--color-primary-light: #22d3ee;
--color-secondary: #1e293b;
--color-accent: #f59e0b;
--color-surface: #f8fafc;
--color-surface-alt: #f1f5f9;
--color-border: #e2e8f0;
--color-border-strong: #cbd5e1;
--color-text-primary: #1e293b;
--color-text-secondary: #475569;
--color-text-muted: #94a3b8;
--color-text-inverse: #ffffff;
/* Typography */
--font-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
/* Border Radius */
--radius-sm: 0.5rem; /* 8px */
--radius-md: 0.75rem; /* 12px */
--radius-lg: 1rem; /* 16px */
--radius-xl: 1.5rem; /* 24px */
--radius-full: 9999px;
/* Shadows */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
--shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);
--shadow-primary: 0 20px 25px -5px rgb(8 145 178 / 0.3);
/* Transitions */
--transition-fast: 150ms ease;
--transition-base: 200ms ease;
--transition-slow: 300ms ease;
--transition-slower: 500ms ease;
/* Spacing */
--section-padding: 6rem; /* py-24 */
--section-padding-sm: 4rem; /* py-16 */
--container-max: 80rem; /* max-w-7xl */
}
/* ============================================================
BASE STYLES
============================================================ */
@layer base {
html {
font-family: var(--font-sans);
scroll-behavior: smooth;
}
body {
@apply bg-white text-secondary-800 antialiased;
}
h1, h2, h3, h4, h5, h6 {
@apply font-bold tracking-tight;
}
code, pre {
font-family: var(--font-mono);
}
/* Image optimization defaults */
img {
height: auto;
max-width: 100%;
/* Prevent images from causing layout shift */
}
/* Improve rendering quality for scaled images */
img[width][height] {
aspect-ratio: attr(width) / attr(height);
}
/* Focus visible for keyboard navigation */
:focus-visible {
@apply outline-none ring-2 ring-primary-400 ring-offset-2;
}
/* Smooth selection highlight */
::selection {
@apply bg-primary-100 text-primary-900;
}
/* Scrollbar (WebKit) */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
@apply bg-secondary-100;
}
::-webkit-scrollbar-thumb {
@apply bg-secondary-300 rounded-full;
}
::-webkit-scrollbar-thumb:hover {
@apply bg-secondary-400;
}
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
html {
scroll-behavior: auto;
}
}
}
/* ============================================================
COMPONENT CLASSES
Reusable utility classes built on design tokens
============================================================ */
@layer components {
/* ----- Layout ----- */
.container-wrapper {
@apply mx-auto max-w-7xl px-4 sm:px-6 lg:px-8;
}
/* ----- Buttons ----- */
.btn-primary {
@apply inline-flex items-center justify-center rounded-lg bg-primary px-6 py-3 font-semibold text-white transition-all hover:bg-primary-600 hover:shadow-lg hover:-translate-y-0.5 active:translate-y-0 active:shadow-none focus:outline-none focus:ring-2 focus:ring-primary-400 focus:ring-offset-2;
position: relative;
overflow: hidden;
}
.btn-primary::after {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(circle at var(--x, 50%) var(--y, 50%), rgba(255,255,255,0.2) 0%, transparent 60%);
opacity: 0;
transition: opacity 300ms ease;
}
.btn-primary:hover::after {
opacity: 1;
}
.btn-secondary {
@apply inline-flex items-center justify-center rounded-lg border-2 border-secondary bg-transparent px-6 py-3 font-semibold text-secondary transition-colors hover:bg-secondary hover:text-white focus:outline-none focus:ring-2 focus:ring-secondary-400 focus:ring-offset-2;
}
.btn-accent {
@apply inline-flex items-center justify-center rounded-lg bg-accent px-6 py-3 font-semibold text-white transition-colors hover:bg-accent-600 focus:outline-none focus:ring-2 focus:ring-accent-400 focus:ring-offset-2;
}
.btn-ghost {
@apply inline-flex items-center justify-center rounded-lg border-2 border-white/30 bg-white/5 backdrop-blur-sm px-6 py-3 font-semibold text-white transition-all hover:bg-white/10 hover:border-white/50 focus:outline-none focus:ring-2 focus:ring-white/50 focus:ring-offset-2;
}
.btn-primary-lg {
@apply inline-flex items-center justify-center rounded-xl bg-gradient-to-r from-primary-500 to-primary-600 px-10 py-5 text-lg font-bold text-white transition-all hover:shadow-2xl hover:shadow-primary/50 hover:scale-105 focus:outline-none focus:ring-2 focus:ring-primary-400 focus:ring-offset-2;
position: relative;
overflow: hidden;
}
.btn-primary-lg::after {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(circle at var(--x, 50%) var(--y, 50%), rgba(255,255,255,0.2) 0%, transparent 60%);
opacity: 0;
transition: opacity 300ms ease;
}
.btn-primary-lg:hover::after {
opacity: 1;
}
.btn-inverse-lg {
@apply inline-flex items-center justify-center rounded-xl bg-white px-10 py-5 text-lg font-bold text-secondary-900 transition-all hover:shadow-2xl hover:scale-105 focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2;
}
/* ----- Badges / Eyebrow Labels ----- */
.badge {
@apply inline-block px-4 py-2 rounded-full text-sm font-bold uppercase tracking-wider;
}
.badge-primary {
@apply badge bg-primary-50 text-primary-700;
}
.badge-primary-dark {
@apply badge bg-primary-400/20 text-primary-300;
}
.badge-accent {
@apply badge bg-accent-50 text-accent-700;
}
.badge-secondary {
@apply badge bg-secondary-100 text-secondary-700;
}
/* ----- Form Elements ----- */
.form-input {
@apply w-full px-4 py-3 rounded-lg border border-secondary-300 focus:border-primary focus:ring-2 focus:ring-primary/20 transition-colors duration-200 text-secondary-900 placeholder-secondary-400 bg-white;
}
.form-input-error {
@apply border-red-500 focus:border-red-500 focus:ring-red-500/20;
}
.form-label {
@apply block text-sm font-medium text-secondary-700 mb-2;
}
.form-error {
@apply mt-1 text-sm text-red-500 hidden;
}
/* ----- Cards ----- */
.card {
@apply bg-white rounded-2xl border-2 border-secondary-100 transition-all duration-500;
}
.card-hover {
@apply card hover:border-primary/30 hover:shadow-2xl hover:-translate-y-2;
}
.card-service {
@apply bg-white rounded-3xl shadow-lg border-2 border-secondary-100 hover:border-primary-400 hover:shadow-2xl transition-all duration-500;
}
.card-surface {
@apply bg-secondary-50 rounded-xl border border-secondary-200 hover:shadow-lg transition-shadow duration-300;
}
/* ----- Icon Containers ----- */
.icon-container {
@apply flex items-center justify-center bg-gradient-to-br from-primary-500 to-primary-600 shadow-lg;
}
.icon-sm {
@apply icon-container w-12 h-12 rounded-lg;
}
.icon-md {
@apply icon-container w-16 h-16 rounded-2xl;
}
.icon-lg {
@apply icon-container w-20 h-20 rounded-2xl;
}
/* ----- Section Patterns ----- */
.section-header {
@apply text-center max-w-3xl mx-auto mb-20;
}
.section-title {
@apply text-4xl sm:text-5xl font-bold text-secondary-900 mb-6;
}
.section-title-inverse {
@apply text-4xl sm:text-5xl font-bold text-white mb-6;
}
.section-description {
@apply text-xl text-secondary-600;
}
.section-description-inverse {
@apply text-xl text-secondary-300;
}
/* ----- Decorative Background Blobs ----- */
.blob {
@apply absolute rounded-full blur-3xl pointer-events-none;
}
.blob-primary {
@apply blob bg-primary/20;
}
.blob-accent {
@apply blob bg-accent/10;
}
/* ----- Dark Section Grid Overlay ----- */
.grid-overlay {
@apply absolute inset-0 bg-[linear-gradient(rgba(255,255,255,0.02)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.02)_1px,transparent_1px)] bg-[size:80px_80px] pointer-events-none;
}
/* ----- Avatar Initials ----- */
.avatar {
@apply flex items-center justify-center bg-gradient-to-br from-primary-500 to-primary-600 text-white font-bold shadow-lg rounded-full;
}
/* ----- Feature List Item ----- */
.feature-item {
@apply flex items-center text-secondary-700;
}
/* ----- Gradient Text ----- */
.gradient-text {
@apply text-transparent bg-clip-text bg-gradient-to-r from-primary-400 via-primary-300 to-accent-400;
}
.gradient-text-primary {
@apply text-transparent bg-clip-text bg-gradient-to-r from-primary to-accent;
}
}
/* ============================================================
SCROLL-REVEAL & MICRO-INTERACTION ANIMATIONS
Used with Intersection Observer for scroll-triggered effects
============================================================ */
/* Base state: elements are hidden until observed */
[data-animate] {
opacity: 0;
transition-property: opacity, transform;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 600ms;
will-change: opacity, transform;
}
/* Animated-in state */
[data-animate].is-visible {
opacity: 1;
transform: none !important;
}
/* Animation variants */
[data-animate="fade-up"] {
transform: translateY(32px);
}
[data-animate="fade-down"] {
transform: translateY(-32px);
}
[data-animate="fade-left"] {
transform: translateX(-32px);
}
[data-animate="fade-right"] {
transform: translateX(32px);
}
[data-animate="fade-in"] {
transform: none;
}
[data-animate="scale-up"] {
transform: scale(0.92);
}
[data-animate="zoom-in"] {
transform: scale(0.85);
transform-origin: center;
}
/* Stagger delay variants */
[data-delay="100"] { transition-delay: 100ms; }
[data-delay="150"] { transition-delay: 150ms; }
[data-delay="200"] { transition-delay: 200ms; }
[data-delay="300"] { transition-delay: 300ms; }
[data-delay="400"] { transition-delay: 400ms; }
[data-delay="500"] { transition-delay: 500ms; }
[data-delay="600"] { transition-delay: 600ms; }
[data-delay="700"] { transition-delay: 700ms; }
[data-delay="800"] { transition-delay: 800ms; }
/* Faster variant for subtle items */
[data-animate][data-duration="fast"] {
transition-duration: 400ms;
}
/* Slower variant for larger elements */
[data-animate][data-duration="slow"] {
transition-duration: 900ms;
}
/* ---- Hover micro-interactions ---- */
/* Magnetic button ripple effect */
.btn-ripple {
position: relative;
overflow: hidden;
}
.btn-ripple::after {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(circle at var(--x, 50%) var(--y, 50%), rgba(255,255,255,0.25) 0%, transparent 60%);
opacity: 0;
transition: opacity 300ms ease;
}
.btn-ripple:hover::after {
opacity: 1;
}
/* Card tilt + glow on hover */
.card-interactive {
transition: transform 400ms cubic-bezier(0.4, 0, 0.2, 1), box-shadow 400ms ease;
transform-style: preserve-3d;
}
.card-interactive:hover {
transform: translateY(-6px) scale(1.01);
box-shadow: 0 20px 40px -12px rgba(8, 145, 178, 0.2);
}
/* Icon bounce on hover */
.icon-bounce:hover svg {
animation: icon-bounce 0.5s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
}
/* Underline slide effect for links */
.link-underline {
position: relative;
}
.link-underline::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 0;
height: 2px;
background: currentColor;
transition: width 300ms cubic-bezier(0.4, 0, 0.2, 1);
}
.link-underline:hover::after {
width: 100%;
}
/* Counter number pulse on enter */
.stat-counter {
transition: transform 200ms ease;
}
.stat-counter.counting {
animation: count-pulse 0.1s ease-out;
}
/* Progress bar fill animation */
.progress-bar {
width: 0;
transition: width 1.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.progress-bar.is-visible {
width: var(--progress-width, 100%);
}
/* Shimmer effect for skeleton/loading states */
@keyframes shimmer {
from { background-position: -200% 0; }
to { background-position: 200% 0; }
}
/* Page transition overlay */
.page-transition-enter {
animation: page-enter 500ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
/* Additional keyframes */
@keyframes icon-bounce {
0%, 100% { transform: translateY(0); }
30% { transform: translateY(-6px); }
60% { transform: translateY(2px); }
}
@keyframes count-pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.08); }
}
@keyframes page-enter {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes draw-in {
from { stroke-dashoffset: 100; }
to { stroke-dashoffset: 0; }
}
/* Respect reduced motion */
@media (prefers-reduced-motion: reduce) {
[data-animate],
[data-animate].is-visible,
.card-interactive:hover,
.link-underline::after,
.btn-ripple::after,
.progress-bar {
transition: none !important;
animation: none !important;
transform: none !important;
opacity: 1 !important;
width: var(--progress-width, 100%) !important;
}
}
/* ============================================================
UTILITY CLASSES
One-off helpers that extend Tailwind
============================================================ */
@layer utilities {
/* Consistent hero gradient backgrounds */
.bg-hero-dark {
@apply bg-gradient-to-br from-secondary-900 via-secondary-800 to-primary-900;
}
.bg-cta-dark {
@apply bg-gradient-to-br from-secondary-900 via-primary-900 to-primary-800;
}
.bg-surface-gradient {
@apply bg-gradient-to-b from-secondary-50 to-white;
}
/* Tabular number rendering for stats/counters */
.tabular-nums {
font-variant-numeric: tabular-nums;
}
/* Line clamp utilities (supplement Tailwind's built-in) */
.line-clamp-2 {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.line-clamp-3 {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
}
}
+106
View File
@@ -0,0 +1,106 @@
/**
* Analytics event tracking utility
*
* Supports both Google Analytics 4 (gtag) and Plausible.
* Safe to call even when analytics is not configured — silently no-ops.
*
* Usage:
* import { trackEvent } from '../utils/analytics';
* trackEvent('form_submit', { form_name: 'contact' });
*/
interface EventParams {
[key: string]: string | number | boolean;
}
/**
* Track a custom event in GA4 and/or Plausible
*/
export function trackEvent(eventName: string, params: EventParams = {}): void {
// Guard: skip if DNT is enabled
if (navigator.doNotTrack === '1' || (window as any).doNotTrack === '1') {
return;
}
// Google Analytics 4
if (typeof (window as any).gtag === 'function') {
(window as any).gtag('event', eventName, params);
}
// Plausible Analytics
if (typeof (window as any).plausible === 'function') {
(window as any).plausible(eventName, { props: params });
}
}
/**
* Track external link clicks
* Call this on anchor elements pointing to external domains.
*/
export function trackExternalLink(url: string, label?: string): void {
trackEvent('external_link_click', {
link_url: url,
link_label: label ?? url,
outbound: true,
});
}
/**
* Track form submission events
*/
export function trackFormSubmit(formName: string, success: boolean): void {
trackEvent(success ? 'form_submit_success' : 'form_submit_error', {
form_name: formName,
});
}
/**
* Track newsletter signup events
*/
export function trackNewsletterSignup(success: boolean): void {
trackEvent(success ? 'newsletter_signup_success' : 'newsletter_signup_error', {
form_name: 'newsletter',
});
}
/**
* Track portfolio filter clicks
*/
export function trackPortfolioFilter(filter: string): void {
trackEvent('portfolio_filter', {
filter_category: filter,
});
}
/**
* Track portfolio case study views
*/
export function trackCaseStudyView(projectId: string, projectTitle: string): void {
trackEvent('case_study_view', {
project_id: projectId,
project_title: projectTitle,
});
}
/**
* Auto-bind external link tracking to all external anchor tags on the page.
* Call once after DOM is ready.
*/
export function bindExternalLinkTracking(): void {
const currentHost = window.location.hostname;
document.querySelectorAll<HTMLAnchorElement>('a[href]').forEach((anchor) => {
try {
const url = new URL(anchor.href, window.location.href);
const isExternal = url.hostname !== currentHost && url.hostname !== '';
if (isExternal) {
anchor.addEventListener('click', () => {
trackExternalLink(url.href, anchor.textContent?.trim() || anchor.getAttribute('aria-label') || url.href);
});
}
} catch {
// Invalid URL, skip
}
});
}
+133
View File
@@ -0,0 +1,133 @@
/**
* Scroll-reveal & micro-interaction utilities
* Uses Intersection Observer — no external libraries required.
*/
/**
* Initialise scroll-reveal for all [data-animate] elements.
* Call once per page after DOM is ready.
*/
export function initScrollReveal(): void {
// Skip entirely when the user prefers reduced motion
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
document.querySelectorAll<HTMLElement>('[data-animate]').forEach((el) => {
el.classList.add('is-visible');
});
return;
}
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
// Unobserve after triggering — animate once
observer.unobserve(entry.target);
}
});
},
{
threshold: 0.12,
rootMargin: '0px 0px -40px 0px',
}
);
document.querySelectorAll('[data-animate]').forEach((el) => observer.observe(el));
}
/**
* Animates a numeric counter from 0 to `target`.
* Attaches to elements with [data-counter] attribute.
*/
export function initCounters(): void {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const counterObserver = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
const el = entry.target as HTMLElement;
const target = parseInt(el.dataset.counter ?? '0', 10);
const duration = parseInt(el.dataset.counterDuration ?? '2000', 10);
const suffix = el.dataset.counterSuffix ?? '';
animateCounter(el, target, duration, suffix);
counterObserver.unobserve(el);
});
},
{ threshold: 0.5 }
);
document.querySelectorAll('[data-counter]').forEach((el) =>
counterObserver.observe(el)
);
}
function animateCounter(
el: HTMLElement,
target: number,
duration: number,
suffix: string
): void {
const start = performance.now();
const step = (now: number) => {
const elapsed = now - start;
const progress = Math.min(elapsed / duration, 1);
// Ease-out cubic
const eased = 1 - Math.pow(1 - progress, 3);
const current = Math.floor(eased * target);
el.textContent = current.toLocaleString() + suffix;
el.classList.toggle('counting', progress < 1);
if (progress < 1) requestAnimationFrame(step);
};
requestAnimationFrame(step);
}
/**
* Adds a radial highlight that follows the cursor on `.btn-ripple` elements.
*/
export function initButtonRipple(): void {
document.querySelectorAll<HTMLElement>('.btn-ripple').forEach((btn) => {
btn.addEventListener('mousemove', (e: MouseEvent) => {
const rect = btn.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 100;
const y = ((e.clientY - rect.top) / rect.height) * 100;
btn.style.setProperty('--x', `${x}%`);
btn.style.setProperty('--y', `${y}%`);
});
});
}
/**
* Fills `.progress-bar` elements to their `--progress-width` CSS var
* once they scroll into view.
*/
export function initProgressBars(): void {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
document.querySelectorAll<HTMLElement>('.progress-bar').forEach((el) => {
el.classList.add('is-visible');
});
return;
}
const pbObserver = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
pbObserver.unobserve(entry.target);
}
});
},
{ threshold: 0.4 }
);
document.querySelectorAll('.progress-bar').forEach((el) => pbObserver.observe(el));
}
/** Convenience: run all animation initialisers in one call. */
export function initAllAnimations(): void {
initScrollReveal();
initCounters();
initButtonRipple();
initProgressBars();
}
+67
View File
@@ -0,0 +1,67 @@
/**
* Image Optimization Utilities
*
* Helper functions for optimizing external image URLs
* and generating responsive image srcsets.
*/
/**
* Optimize Unsplash URL with WebP format and quality settings
*/
export function optimizeUnsplashUrl(
url: string,
options: {
width?: number;
height?: number;
quality?: number;
format?: 'webp' | 'auto';
} = {}
): string {
if (!url.includes('unsplash.com')) return url;
const { width, height, quality = 80, format = 'webp' } = options;
// Parse existing URL
const urlObj = new URL(url);
// Set optimization parameters
if (width) urlObj.searchParams.set('w', width.toString());
if (height) urlObj.searchParams.set('h', height.toString());
urlObj.searchParams.set('q', quality.toString());
urlObj.searchParams.set('fm', format);
urlObj.searchParams.set('fit', 'crop');
urlObj.searchParams.set('auto', 'format,compress');
return urlObj.toString();
}
/**
* Generate srcset for responsive images
*/
export function generateSrcset(
url: string,
widths: number[] = [320, 640, 960, 1280],
aspectRatio?: number
): string {
if (!url.includes('unsplash.com')) return '';
return widths
.map((w) => {
const height = aspectRatio ? Math.round(w / aspectRatio) : undefined;
const optimizedUrl = optimizeUnsplashUrl(url, { width: w, height });
return `${optimizedUrl} ${w}w`;
})
.join(', ');
}
/**
* Default image sizes attribute for responsive images
*/
export const defaultSizes = '(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw';
/**
* Generate blur placeholder data URL (10x10 pixel)
*/
export function getBlurPlaceholder(color: string = '#1e293b'): string {
return `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10'%3E%3Crect fill='${encodeURIComponent(color)}' width='10' height='10'/%3E%3C/svg%3E`;
}
+115
View File
@@ -0,0 +1,115 @@
/**
* Structured Logger
*
* Provides consistent, structured logging across the application.
* - In development: pretty-prints to console with colors
* - In production: outputs JSON for log aggregation (Logtail, Datadog, etc.)
*
* Usage:
* import { logger } from '@/utils/logger';
* logger.info('api.contact', 'Form submitted', { email: '...' });
* logger.error('api.contact', 'Email failed', { error: err.message });
*/
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
interface LogEntry {
level: LogLevel;
scope: string;
message: string;
timestamp: string;
[key: string]: unknown;
}
const LEVELS: Record<LogLevel, number> = {
debug: 0,
info: 1,
warn: 2,
error: 3,
};
// Minimum log level — override via LOG_LEVEL env var
function getMinLevel(): LogLevel {
const env = (typeof process !== 'undefined' && process.env.LOG_LEVEL) ||
(typeof import.meta !== 'undefined' && (import.meta as { env?: { LOG_LEVEL?: string } }).env?.LOG_LEVEL);
if (env && env in LEVELS) return env as LogLevel;
return (typeof import.meta !== 'undefined' && (import.meta as { env?: { PROD?: boolean } }).env?.PROD)
? 'info'
: 'debug';
}
function shouldLog(level: LogLevel): boolean {
return LEVELS[level] >= LEVELS[getMinLevel()];
}
function formatEntry(level: LogLevel, scope: string, message: string, meta?: Record<string, unknown>): LogEntry {
return {
level,
scope,
message,
timestamp: new Date().toISOString(),
...meta,
};
}
const isProd = typeof import.meta !== 'undefined'
? (import.meta as { env?: { PROD?: boolean } }).env?.PROD === true
: process.env.NODE_ENV === 'production';
const COLORS: Record<LogLevel, string> = {
debug: '\x1b[90m', // gray
info: '\x1b[36m', // cyan
warn: '\x1b[33m', // yellow
error: '\x1b[31m', // red
};
const RESET = '\x1b[0m';
function output(entry: LogEntry): void {
if (isProd) {
// JSON output for log aggregation in production
// eslint-disable-next-line no-console
console.log(JSON.stringify(entry));
} else {
const color = COLORS[entry.level] ?? '';
const { level, scope, message, timestamp, ...rest } = entry;
const meta = Object.keys(rest).length > 0 ? ' ' + JSON.stringify(rest) : '';
// eslint-disable-next-line no-console
console.log(`${color}[${level.toUpperCase()}]${RESET} ${timestamp} [${scope}] ${message}${meta}`);
}
}
function log(level: LogLevel, scope: string, message: string, meta?: Record<string, unknown>): void {
if (!shouldLog(level)) return;
output(formatEntry(level, scope, message, meta));
}
export const logger = {
debug: (scope: string, message: string, meta?: Record<string, unknown>) => log('debug', scope, message, meta),
info: (scope: string, message: string, meta?: Record<string, unknown>) => log('info', scope, message, meta),
warn: (scope: string, message: string, meta?: Record<string, unknown>) => log('warn', scope, message, meta),
error: (scope: string, message: string, meta?: Record<string, unknown>) => log('error', scope, message, meta),
};
/**
* Log an API request/response pair. Use at the end of API handlers.
*/
export function logApiRequest(opts: {
scope: string;
method: string;
path: string;
status: number;
ip?: string;
durationMs?: number;
meta?: Record<string, unknown>;
}): void {
const { scope, method, path, status, ip, durationMs, meta } = opts;
const level: LogLevel = status >= 500 ? 'error' : status >= 400 ? 'warn' : 'info';
log(level, scope, `${method} ${path} ${status}`, {
method,
path,
status,
...(ip && { ip }),
...(durationMs !== undefined && { durationMs }),
...meta,
});
}
+218
View File
@@ -0,0 +1,218 @@
/**
* Sentry Error Tracking — Optional Integration
*
* Activated only when SENTRY_DSN environment variable is set.
* Uses the Sentry REST API directly (no SDK dependency required).
*
* For full SDK integration, install @sentry/node and replace captureException
* with the SDK's init() + Sentry.captureException().
*
* Usage:
* import { captureException, captureMessage } from '@/utils/sentry';
* captureException(error, { scope: 'api.contact', extra: { ip } });
* captureMessage('Rate limit hit', 'warning', { scope: 'api.contact' });
*/
import { logger } from './logger';
interface SentryContext {
scope?: string; // e.g. 'api.contact'
user?: { ip?: string; email?: string };
extra?: Record<string, unknown>;
tags?: Record<string, string>;
}
interface SentryEnvelope {
event_id: string;
timestamp: string;
platform: string;
level: string;
environment: string;
release?: string;
transaction?: string;
logger?: string;
exception?: {
values: Array<{
type: string;
value: string;
stacktrace?: { frames: Array<{ filename?: string; function?: string; lineno?: number }> };
}>;
};
message?: string;
user?: { ip_address?: string; email?: string };
extra?: Record<string, unknown>;
tags?: Record<string, string>;
}
function getSentryDsn(): string | undefined {
try {
return (import.meta as { env?: { SENTRY_DSN?: string } }).env?.SENTRY_DSN;
} catch {
return process.env.SENTRY_DSN;
}
}
function getEnvironment(): string {
try {
return (import.meta as { env?: { PROD?: boolean } }).env?.PROD ? 'production' : 'development';
} catch {
return process.env.NODE_ENV ?? 'development';
}
}
function getRelease(): string | undefined {
try {
return process.env.npm_package_version ?? process.env.RELEASE_VERSION;
} catch {
return undefined;
}
}
function generateEventId(): string {
return Array.from({ length: 32 }, () => Math.floor(Math.random() * 16).toString(16)).join('');
}
/** Parse DSN into project URL and auth header */
function parseDsn(dsn: string): { url: string; auth: string } | null {
try {
const url = new URL(dsn);
const key = url.username;
const projectId = url.pathname.replace('/', '');
const storeUrl = `${url.protocol}//${url.host}/api/${projectId}/store/`;
return { url: storeUrl, auth: `Sentry sentry_version=7, sentry_key=${key}` };
} catch {
return null;
}
}
async function sendToSentry(payload: SentryEnvelope): Promise<void> {
const dsn = getSentryDsn();
if (!dsn) return;
const parsed = parseDsn(dsn);
if (!parsed) {
logger.warn('sentry', 'Invalid SENTRY_DSN format — skipping event');
return;
}
try {
const res = await fetch(parsed.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Sentry-Auth': parsed.auth,
},
body: JSON.stringify(payload),
});
if (!res.ok) {
logger.warn('sentry', 'Failed to send event to Sentry', { status: res.status });
}
} catch (err) {
// Never let Sentry reporting crash the application
logger.warn('sentry', 'Network error sending to Sentry', {
error: err instanceof Error ? err.message : String(err),
});
}
}
/**
* Capture an exception and send to Sentry (if configured).
* Also logs the error via the structured logger.
*/
export async function captureException(
error: unknown,
context: SentryContext = {}
): Promise<void> {
const err = error instanceof Error ? error : new Error(String(error));
const scope = context.scope ?? 'app';
// Always log locally
logger.error(scope, err.message, {
error: err.name,
stack: err.stack?.split('\n').slice(0, 5).join(' | '),
...context.extra,
});
// Send to Sentry if DSN is configured
const dsn = getSentryDsn();
if (!dsn) return;
const frames = err.stack
?.split('\n')
.slice(1)
.map((line) => {
const match = line.trim().match(/at\s+(.+?)\s+\((.+?):(\d+):\d+\)/) ??
line.trim().match(/at\s+(.+?):(\d+):\d+/);
if (match) {
return {
function: match[1],
filename: match[2] ?? match[1],
lineno: parseInt(match[3] ?? match[2], 10),
};
}
return { filename: line.trim() };
}) ?? [];
await sendToSentry({
event_id: generateEventId(),
timestamp: new Date().toISOString(),
platform: 'node',
level: 'error',
environment: getEnvironment(),
release: getRelease(),
logger: scope,
transaction: scope,
exception: {
values: [{
type: err.name,
value: err.message,
stacktrace: { frames },
}],
},
user: context.user
? { ip_address: context.user.ip, email: context.user.email }
: undefined,
extra: context.extra,
tags: context.tags,
});
}
/**
* Capture a message (non-exception) and send to Sentry (if configured).
* Also logs the message via the structured logger.
*/
export async function captureMessage(
message: string,
level: 'info' | 'warning' | 'error' = 'info',
context: SentryContext = {}
): Promise<void> {
const scope = context.scope ?? 'app';
if (level === 'error') {
logger.error(scope, message, context.extra);
} else if (level === 'warning') {
logger.warn(scope, message, context.extra);
} else {
logger.info(scope, message, context.extra);
}
const dsn = getSentryDsn();
if (!dsn) return;
await sendToSentry({
event_id: generateEventId(),
timestamp: new Date().toISOString(),
platform: 'node',
level: level === 'warning' ? 'warning' : level,
environment: getEnvironment(),
release: getRelease(),
logger: scope,
message,
user: context.user
? { ip_address: context.user.ip, email: context.user.email }
: undefined,
extra: context.extra,
tags: context.tags,
});
}
+223
View File
@@ -0,0 +1,223 @@
// SEO Utility Functions
// Helpers for generating meta tags, structured data, and canonical URLs
export interface SEOMetadata {
title: string;
description: string;
keywords?: string[];
canonicalUrl?: string;
ogImage?: string;
ogType?: 'website' | 'article' | 'profile';
twitterCard?: 'summary' | 'summary_large_image' | 'player';
publishedTime?: string;
modifiedTime?: string;
author?: string;
noIndex?: boolean;
}
/**
* Generate page title with site name
*/
export function generatePageTitle(title: string, siteName: string = 'WorkRoot IT Solutions'): string {
if (title === 'Home' || title.includes(siteName)) {
return siteName;
}
return `${title} | ${siteName}`;
}
/**
* Generate keywords from tags and categories
*/
export function generateKeywords(tags: string[], additionalKeywords: string[] = []): string {
const baseKeywords = [
'web development',
'mobile apps',
'AI solutions',
'cloud services',
'IT consulting',
];
const allKeywords = [...new Set([...baseKeywords, ...tags, ...additionalKeywords])];
return allKeywords.join(', ');
}
/**
* Generate breadcrumb structured data
*/
export function generateBreadcrumbSchema(breadcrumbs: Array<{ name: string; url: string }>, siteUrl: string = 'https://workroot.in') {
return {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: breadcrumbs.map((item, index) => ({
'@type': 'ListItem',
position: index + 1,
name: item.name,
item: `${siteUrl}${item.url}`,
})),
};
}
/**
* Generate Article structured data for blog posts
*/
export function generateArticleSchema(
title: string,
description: string,
author: string,
publishedTime: string,
modifiedTime: string,
imageUrl: string,
url: string,
keywords: string[] = []
) {
return {
'@context': 'https://schema.org',
'@type': 'Article',
headline: title,
description: description,
image: imageUrl,
datePublished: publishedTime,
dateModified: modifiedTime,
author: {
'@type': 'Person',
name: author,
},
publisher: {
'@type': 'Organization',
name: 'WorkRoot IT Solutions',
logo: {
'@type': 'ImageObject',
url: 'https://workroot.in/logo.png',
},
},
mainEntityOfPage: {
'@type': 'WebPage',
'@id': url,
},
keywords: keywords.join(', '),
};
}
/**
* Generate FAQ structured data
*/
export function generateFAQSchema(faqs: Array<{ question: string; answer: string }>) {
return {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faqs.map((faq) => ({
'@type': 'Question',
name: faq.question,
acceptedAnswer: {
'@type': 'Answer',
text: faq.answer,
},
})),
};
}
/**
* Generate Service structured data
*/
export function generateServiceSchema(
serviceName: string,
description: string,
features: string[] = [],
siteUrl: string = 'https://workroot.in'
) {
return {
'@context': 'https://schema.org',
'@type': 'Service',
name: serviceName,
description: description,
provider: {
'@type': 'Organization',
name: 'WorkRoot IT Solutions',
url: siteUrl,
},
areaServed: {
'@type': 'GeoShape',
name: 'Worldwide',
},
serviceType: serviceName,
offers: {
'@type': 'Offer',
availability: 'https://schema.org/InStock',
},
hasOfferCatalog: {
'@type': 'OfferCatalog',
name: `${serviceName} Features`,
itemListElement: features.map((feature, index) => ({
'@type': 'Offer',
itemOffered: {
'@type': 'Service',
name: feature,
},
})),
},
};
}
/**
* Generate canonical URL
*/
export function generateCanonicalUrl(pathname: string, siteUrl: string = 'https://workroot.in'): string {
// Remove trailing slash for consistency (except for root)
const cleanPath = pathname === '/' ? pathname : pathname.replace(/\/$/, '');
return `${siteUrl}${cleanPath}`;
}
/**
* Truncate description to optimal length
*/
export function truncateDescription(description: string, maxLength: number = 160): string {
if (description.length <= maxLength) {
return description;
}
// Find last complete word before maxLength
const truncated = description.substring(0, maxLength);
const lastSpace = truncated.lastIndexOf(' ');
return truncated.substring(0, lastSpace) + '...';
}
/**
* Generate Open Graph image URL
*/
export function generateOGImageUrl(imagePath: string, siteUrl: string = 'https://workroot.in'): string {
if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) {
return imagePath;
}
return `${siteUrl}${imagePath.startsWith('/') ? imagePath : `/${imagePath}`}`;
}
/**
* Check if URL should be indexed
*/
export function shouldIndexPage(pathname: string): boolean {
const noIndexPatterns = [
'/admin',
'/private',
'/api',
'/preview',
'/draft',
];
return !noIndexPatterns.some(pattern => pathname.startsWith(pattern));
}
/**
* Generate social media share URLs
*/
export function generateShareUrls(url: string, title: string) {
const encodedUrl = encodeURIComponent(url);
const encodedTitle = encodeURIComponent(title);
return {
twitter: `https://twitter.com/intent/tweet?url=${encodedUrl}&text=${encodedTitle}`,
facebook: `https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`,
linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${encodedUrl}`,
email: `mailto:?subject=${encodedTitle}&body=${encodedUrl}`,
};
}