Add Results in Numbers, Service Selector, and Popular Bundles to Services page

- Metrics section: 4 gradient stat cards with spring entrance animation
- Interactive selector: 3-step wizard (goal → budget → recommendations)
- Popular Bundles: 3 package tiers (Starter, Growth, Full Stack) with CTA
- Full responsive support for all new sections

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Vadym Samoilenko 2026-03-08 15:58:14 +00:00
parent a0cec68ff2
commit 9d281920e9
2 changed files with 670 additions and 3 deletions

View file

@ -274,6 +274,401 @@
font-weight: 600;
}
/* ── Results in Numbers ── */
.metrics-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1.5rem;
max-width: 1200px;
margin: 0 auto;
}
.metric-card {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 20px;
padding: 2rem 1.5rem;
text-align: center;
display: flex;
flex-direction: column;
gap: 0.5rem;
position: relative;
overflow: hidden;
transition: border-color 0.3s, box-shadow 0.4s, transform 0.35s;
}
.metric-card::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: radial-gradient(circle at 30% 20%, rgba(255, 91, 4, 0.08), transparent 60%);
opacity: 0;
transition: opacity 0.4s;
pointer-events: none;
border-radius: 20px;
}
.metric-card:hover {
border-color: rgba(255, 255, 255, 0.2);
transform: translateY(-3px);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
}
.metric-card:hover::before { opacity: 1; }
.metric-value {
font-size: clamp(2rem, 4vw, 2.8rem);
font-weight: 900;
line-height: 1;
background: linear-gradient(135deg, var(--orange-100), var(--yellow-100));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.metric-label {
color: var(--light-grey-100);
font-size: 0.9rem;
opacity: 0.85;
line-height: 1.4;
}
/* ── Service Selector ── */
.selector-section .container {
max-width: 800px;
}
.selector-steps {
display: flex;
justify-content: center;
gap: 2rem;
margin-bottom: 2.5rem;
}
.selector-step-dot {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.4rem;
opacity: 0.35;
transition: opacity 0.3s;
}
.selector-step-dot--active { opacity: 1; }
.selector-step-num {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid rgba(255, 255, 255, 0.15);
color: var(--light-grey-100);
font-weight: 700;
font-size: 0.85rem;
transition: all 0.3s;
}
.selector-step-dot--active .selector-step-num {
border-color: var(--orange-100);
background: var(--orange-100);
color: #fff;
}
.selector-step-label {
font-size: 0.75rem;
color: var(--light-grey-100);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.selector-panel {
text-align: center;
min-height: 200px;
}
.selector-question {
font-size: 1.15rem;
color: #fff;
font-weight: 600;
margin-bottom: 1.5rem;
}
.selector-options {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.75rem;
}
.selector-pill {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 100px;
padding: 0.75rem 1.5rem;
color: var(--light-grey-100);
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
font-family: var(--font-primary);
transition: border-color 0.3s, box-shadow 0.3s, background 0.3s;
}
.selector-pill:hover {
border-color: rgba(255, 91, 4, 0.4);
background: rgba(255, 91, 4, 0.08);
}
.selector-pill--active {
border-color: var(--orange-100);
background: rgba(255, 91, 4, 0.12);
color: var(--orange-100);
box-shadow: 0 0 20px rgba(255, 91, 4, 0.15);
}
.selector-results {
display: flex;
flex-direction: column;
gap: 0.75rem;
max-width: 600px;
margin: 0 auto 1.5rem;
}
.selector-result-card {
display: flex;
align-items: center;
gap: 1rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
padding: 1rem 1.25rem;
text-align: left;
transition: border-color 0.3s;
}
.selector-result-card:hover {
border-color: rgba(255, 91, 4, 0.3);
}
.selector-result-icon {
width: 28px;
height: 28px;
color: var(--orange-100);
flex-shrink: 0;
}
.selector-result-icon svg { width: 100%; height: 100%; }
.selector-result-card h4 {
color: #fff;
font-size: 0.95rem;
font-weight: 700;
margin-bottom: 0.15rem;
}
.selector-result-price {
color: var(--orange-100);
font-size: 0.85rem;
font-weight: 600;
}
.selector-actions {
display: flex;
justify-content: center;
gap: 1rem;
align-items: center;
}
.selector-reset {
background: none;
border: none;
color: var(--light-grey-100);
opacity: 0.7;
font-size: 0.9rem;
cursor: pointer;
font-family: var(--font-primary);
text-decoration: underline;
transition: opacity 0.2s;
}
.selector-reset:hover { opacity: 1; }
.selector-back {
display: block;
background: none;
border: none;
color: var(--light-grey-100);
font-size: 0.9rem;
cursor: pointer;
font-family: var(--font-primary);
margin: 1rem auto 0;
opacity: 0.7;
transition: opacity 0.2s;
}
.selector-back:hover { opacity: 1; }
.selector-no-results {
color: var(--light-grey-100);
opacity: 0.7;
font-size: 0.95rem;
}
/* ── Popular Bundles ── */
.bundles-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
max-width: 1200px;
margin: 0 auto;
align-items: start;
}
.bundle-card {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 20px;
padding: 2rem;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
transition: border-color 0.3s, box-shadow 0.4s, transform 0.35s;
}
.bundle-card::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: radial-gradient(circle at 50% 0%, rgba(255, 91, 4, 0.06), transparent 60%);
opacity: 0;
transition: opacity 0.4s;
pointer-events: none;
}
.bundle-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
}
.bundle-card:hover::before { opacity: 1; }
.bundle-card--popular {
border-color: var(--orange-100);
background: rgba(255, 91, 4, 0.06);
}
.bundle-card--popular::before { opacity: 0.5; }
.bundle-card--popular:hover {
box-shadow: 0 8px 40px rgba(255, 91, 4, 0.15);
}
.bundle-popular-badge {
position: absolute;
top: -12px;
left: 50%;
transform: translateX(-50%);
background: linear-gradient(135deg, var(--orange-100), var(--yellow-100));
color: #fff;
padding: 0.3rem 1rem;
border-radius: 100px;
font-size: 0.8rem;
font-weight: 700;
white-space: nowrap;
box-shadow: 0 2px 10px rgba(255, 91, 4, 0.3);
}
.bundle-name {
font-size: 1.3rem;
font-weight: 700;
color: #fff;
margin-bottom: 0.25rem;
}
.bundle-tagline {
font-size: 0.9rem;
color: var(--light-grey-100);
opacity: 0.75;
margin-bottom: 1.25rem;
font-style: italic;
}
.bundle-services {
list-style: none;
padding: 0;
margin: 0 0 1.5rem 0;
flex: 1;
border-top: 1px solid rgba(255, 255, 255, 0.08);
padding-top: 1rem;
}
.bundle-services li {
color: var(--light-grey-100);
font-size: 0.9rem;
line-height: 1.5;
padding: 0.35rem 0 0.35rem 1.4rem;
position: relative;
}
.bundle-services li::before {
content: '→';
position: absolute;
left: 0;
color: var(--yellow-100);
font-weight: 700;
}
.bundle-price {
font-size: 1.5rem;
font-weight: 900;
background: linear-gradient(135deg, var(--orange-100), var(--yellow-100));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 1.25rem;
}
.bundle-cta {
display: inline-block;
text-align: center;
background: rgba(255, 91, 4, 0.15);
color: var(--orange-100);
border: 1px solid rgba(255, 91, 4, 0.3);
padding: 0.75rem 1.5rem;
border-radius: 100px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
font-family: var(--font-primary);
transition: background 0.3s, box-shadow 0.3s;
}
.bundle-cta:hover {
background: rgba(255, 91, 4, 0.25);
}
.bundle-cta--popular {
background: var(--orange-100);
color: #fff;
border-color: var(--orange-100);
box-shadow: 0 4px 15px rgba(255, 91, 4, 0.3);
}
.bundle-cta--popular:hover {
box-shadow: 0 8px 25px rgba(255, 91, 4, 0.45);
}
/* Scroll margin for selector "View Services" */
.services-grid {
scroll-margin-top: 10rem;
}
/* CTA */
.services-cta-section {
padding-bottom: 6rem;
@ -342,6 +737,15 @@
.services-grid {
grid-template-columns: 1fr;
}
.metrics-grid {
grid-template-columns: repeat(2, 1fr);
}
.bundles-grid {
grid-template-columns: 1fr;
max-width: 450px;
}
}
@media (max-width: 900px) {
@ -368,6 +772,35 @@
grid-template-columns: 1fr;
}
.metrics-grid {
gap: 1rem;
}
.metric-card {
padding: 1.5rem 1rem;
}
.metric-value {
font-size: 1.8rem;
}
.selector-steps {
gap: 1rem;
}
.selector-step-label {
display: none;
}
.selector-pill {
padding: 0.6rem 1.2rem;
font-size: 0.85rem;
}
.bundles-grid {
max-width: 100%;
}
.services-cta {
padding: 2.5rem 1.5rem;
}

View file

@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { motion } from 'framer-motion';
import { useEffect, useState, useRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Helmet } from 'react-helmet-async';
import SEO from '../components/SEO';
import Modal from '../components/Modal';
@ -173,10 +173,87 @@ const assurancePack = [
'Handover & transition plan',
];
const metrics = [
{ value: '3050%', label: 'Reduction in manual work' },
{ value: '28 wks', label: 'Average delivery time' },
{ value: '24/7', label: 'Automated uptime' },
{ value: '£0', label: 'Vendor lock-in fees' },
];
type GoalKey = 'automate' | 'leads' | 'presence' | 'connect' | 'ai';
type BudgetKey = 'under5k' | '5k-10k' | '10k+';
const goalOptions: { key: GoalKey; label: string }[] = [
{ key: 'automate', label: 'Automate workflows' },
{ key: 'leads', label: 'Get more leads' },
{ key: 'presence', label: 'Build online presence' },
{ key: 'connect', label: 'Connect my tools' },
{ key: 'ai', label: 'Add AI capabilities' },
];
const budgetOptions: { key: BudgetKey; label: string }[] = [
{ key: 'under5k', label: 'Under £5K' },
{ key: '5k-10k', label: '£5K £10K' },
{ key: '10k+', label: '£10K+' },
];
const goalToServices: Record<GoalKey, string[]> = {
automate: ['Workflow Automation Implementation', 'System Integration & Synchronisation'],
leads: ['AI Chatbots & Virtual Assistants', 'CRM Workflow Optimisation', 'Marketing Automation Setup'],
presence: ['Custom Website Development'],
connect: ['System Integration & Synchronisation', 'Infrastructure Setup & Configuration'],
ai: ['AI Integration & Enhancement', 'AI Chatbots & Virtual Assistants'],
};
const budgetFilter: Record<BudgetKey, string[]> = {
'under5k': [
'Infrastructure Setup & Configuration', 'CRM Workflow Optimisation',
'Custom Website Development', 'System Integration & Synchronisation',
'AI Chatbots & Virtual Assistants',
],
'5k-10k': [
'Infrastructure Setup & Configuration', 'CRM Workflow Optimisation',
'Custom Website Development', 'System Integration & Synchronisation',
'AI Chatbots & Virtual Assistants', 'Workflow Automation Implementation',
'Marketing Automation Setup', 'AI Integration & Enhancement',
],
'10k+': services.map(s => s.title),
};
const bundles = [
{
name: 'Starter',
tagline: 'Launch & Engage',
services: ['Custom Website Development', 'AI Chatbots & Virtual Assistants'],
price: 'from £5,500',
},
{
name: 'Growth',
tagline: 'Scale & Convert',
services: ['CRM Workflow Optimisation', 'Marketing Automation Setup', 'System Integration & Synchronisation'],
price: 'from £9,000',
popular: true,
},
{
name: 'Full Stack',
tagline: 'Transform Everything',
services: services.map(s => s.title),
price: 'Custom pricing',
},
];
const ServicesPage = () => {
useEffect(() => { window.scrollTo(0, 0); }, []);
const [isModalOpen, setIsModalOpen] = useState(false);
const [expandedCard, setExpandedCard] = useState<number | null>(null);
const [selectorStep, setSelectorStep] = useState(0);
const [selectedGoal, setSelectedGoal] = useState<GoalKey | null>(null);
const [selectedBudget, setSelectedBudget] = useState<BudgetKey | null>(null);
const serviceCardsRef = useRef<HTMLDivElement>(null);
const recommendedServices = selectedGoal && selectedBudget
? goalToServices[selectedGoal].filter(s => budgetFilter[selectedBudget].includes(s))
: [];
return (
<div className="services-page">
@ -225,7 +302,7 @@ const ServicesPage = () => {
{/* Service Cards */}
<section className="services-section">
<div className="container">
<div className="services-grid">
<div className="services-grid" ref={serviceCardsRef}>
{services.map((s, i) => {
const isExpanded = expandedCard === i;
return (
@ -299,6 +376,163 @@ const ServicesPage = () => {
</div>
</section>
{/* Results in Numbers */}
<section className="services-section">
<div className="container">
<motion.h2 className="section-title" {...fadeUp}>Results in Numbers</motion.h2>
<div className="metrics-grid">
{metrics.map((m, i) => (
<motion.div
key={m.label}
className="metric-card"
initial={{ opacity: 0, scale: 0.5 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true, margin: '-60px' }}
transition={{ type: 'spring', stiffness: 200, damping: 15, delay: i * 0.1 }}
>
<span className="metric-value">{m.value}</span>
<span className="metric-label">{m.label}</span>
</motion.div>
))}
</div>
</div>
</section>
{/* Which Service Do You Need? */}
<section className="services-section selector-section">
<div className="container">
<motion.h2 className="section-title" {...fadeUp}>Which Service Do You Need?</motion.h2>
<div className="selector-steps">
{['Your Goal', 'Budget', 'Results'].map((label, i) => (
<div key={label} className={`selector-step-dot ${selectorStep >= i ? 'selector-step-dot--active' : ''}`}>
<span className="selector-step-num">{i + 1}</span>
<span className="selector-step-label">{label}</span>
</div>
))}
</div>
<AnimatePresence mode="wait">
{selectorStep === 0 && (
<motion.div key="goal" className="selector-panel" initial={{ opacity: 0, x: 40 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -40 }} transition={{ duration: 0.3 }}>
<p className="selector-question">What's your primary goal?</p>
<div className="selector-options">
{goalOptions.map((opt) => (
<motion.button
key={opt.key}
className={`selector-pill ${selectedGoal === opt.key ? 'selector-pill--active' : ''}`}
onClick={() => { setSelectedGoal(opt.key); setSelectorStep(1); }}
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
>
{opt.label}
</motion.button>
))}
</div>
</motion.div>
)}
{selectorStep === 1 && (
<motion.div key="budget" className="selector-panel" initial={{ opacity: 0, x: 40 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -40 }} transition={{ duration: 0.3 }}>
<p className="selector-question">What's your budget range?</p>
<div className="selector-options">
{budgetOptions.map((opt) => (
<motion.button
key={opt.key}
className={`selector-pill ${selectedBudget === opt.key ? 'selector-pill--active' : ''}`}
onClick={() => { setSelectedBudget(opt.key); setSelectorStep(2); }}
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
>
{opt.label}
</motion.button>
))}
</div>
</motion.div>
)}
{selectorStep === 2 && (
<motion.div key="results" className="selector-panel" initial={{ opacity: 0, x: 40 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -40 }} transition={{ duration: 0.3 }}>
<p className="selector-question">We recommend these services:</p>
<div className="selector-results">
{recommendedServices.length > 0 ? recommendedServices.map((sTitle) => {
const svc = services.find(s => s.title === sTitle);
if (!svc) return null;
return (
<div key={sTitle} className="selector-result-card">
<div className="selector-result-icon">{svc.icon}</div>
<div>
<h4>{svc.title}</h4>
<span className="selector-result-price">{svc.price}</span>
</div>
</div>
);
}) : (
<p className="selector-no-results">No exact match but we can help! Contact us for a custom solution.</p>
)}
</div>
<div className="selector-actions">
<motion.button
className="services-cta-btn"
onClick={() => serviceCardsRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })}
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
>
View Services
</motion.button>
<button className="selector-reset" onClick={() => { setSelectorStep(0); setSelectedGoal(null); setSelectedBudget(null); }}>
Start Over
</button>
</div>
</motion.div>
)}
</AnimatePresence>
{selectorStep > 0 && selectorStep < 2 && (
<button className="selector-back" onClick={() => setSelectorStep(s => s - 1)}>
Back
</button>
)}
</div>
</section>
{/* Popular Bundles */}
<section className="services-section">
<div className="container">
<motion.h2 className="section-title" {...fadeUp}>Popular Bundles</motion.h2>
<div className="bundles-grid">
{bundles.map((b, i) => (
<motion.div
key={b.name}
className={`bundle-card ${'popular' in b && b.popular ? 'bundle-card--popular' : ''}`}
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-60px' }}
transition={{ duration: 0.5, delay: i * 0.1 }}
>
{'popular' in b && b.popular && <span className="bundle-popular-badge">Most Popular</span>}
<h3 className="bundle-name">{b.name}</h3>
<p className="bundle-tagline">{b.tagline}</p>
<ul className="bundle-services">
{b.services.map(s => (
<li key={s}>{s}</li>
))}
</ul>
<div className="bundle-price">{b.price}</div>
<motion.button
className={`bundle-cta ${'popular' in b && b.popular ? 'bundle-cta--popular' : ''}`}
onClick={() => setIsModalOpen(true)}
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
>
Get Started
</motion.button>
</motion.div>
))}
</div>
</div>
</section>
{/* CTA */}
<section className="services-section services-cta-section">
<div className="container">