Files
resolutionflow/frontend/src/pages/LandingPage.tsx
chihlasm bc43f53633 feat: overdrive landing page — live chat animation + scroll-driven reveals
A) Live App Preview:
- Chat messages animate in with staggered timing (0.6s apart)
- Typing indicator with bouncing dots appears before AI response,
  then fades out as the response lines arrive
- Sidebar items stagger in during the entrance sequence
- Creates a "show don't tell" demo moment in the hero

B) Scroll-Driven Enhancements (@supports animation-timeline):
- Sections use CSS scroll-driven animations instead of JS IntersectionObserver
- Problem cards, feature cards, pricing cards, and step cards stagger
  within their parent as they enter the viewport
- Social proof bar has subtle parallax drift
- Falls back to existing JS-based reveal for Firefox/older browsers

Accessibility:
- prefers-reduced-motion removes all chat animations, shows content
  immediately, hides typing indicator entirely

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 02:13:41 +00:00

567 lines
30 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useCallback, useRef } from 'react'
import { Link } from 'react-router-dom'
import { PageMeta } from '@/components/common/PageMeta'
import '@/styles/landing.css'
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'
export default function LandingPage() {
const [navScrolled, setNavScrolled] = useState(false)
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
const [betaEmail, setBetaEmail] = useState('')
const [betaStatus, setBetaStatus] = useState<'idle' | 'sending' | 'sent' | 'error'>('idle')
const mobileMenuRef = useRef<HTMLDivElement>(null)
// Nav scroll effect
useEffect(() => {
const handleScroll = () => setNavScrolled(window.scrollY > 40)
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}, [])
// Close mobile menu on click outside
useEffect(() => {
function handleClickOutside(e: MouseEvent) {
if (mobileMenuRef.current && !mobileMenuRef.current.contains(e.target as Node)) {
setMobileMenuOpen(false)
}
}
if (mobileMenuOpen) {
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}
}, [mobileMenuOpen])
// Close mobile menu on scroll to section
const handleMobileNavClick = () => setMobileMenuOpen(false)
// Scroll reveal
useEffect(() => {
const els = document.querySelectorAll('.landing-reveal')
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) entry.target.classList.add('visible')
})
},
{ threshold: 0.15 }
)
els.forEach(el => observer.observe(el))
return () => observer.disconnect()
}, [])
const handleBetaSubmit = useCallback(async (e: React.FormEvent) => {
e.preventDefault()
if (!betaEmail.trim() || betaStatus === 'sending') return
setBetaStatus('sending')
try {
const resp = await fetch(`${API_URL}/api/v1/beta-signup`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: betaEmail }),
})
if (!resp.ok) throw new Error('Signup failed')
setBetaStatus('sent')
setBetaEmail('')
} catch {
setBetaStatus('error')
setTimeout(() => setBetaStatus('idle'), 3000)
}
}, [betaEmail, betaStatus])
return (
<>
<PageMeta
title="ResolutionFlow — From Issue to Resolution, Documented"
description="Your AI troubleshooting copilot. Describe the issue, get help fixing it, and get clean ticket notes — automatically."
/>
<div className="landing-page">
<div className="landing-ambient-glow" />
<div className="landing-grid-pattern" />
<div className="landing-page-content">
{/* Navigation */}
<nav className={`landing-nav ${navScrolled ? 'scrolled' : ''}`} ref={mobileMenuRef}>
<div className="landing-nav-inner">
<a href="#" className="landing-nav-logo">
<div className="landing-nav-logo-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="#000" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="5" r="2"/>
<line x1="12" y1="7" x2="12" y2="11"/>
<circle cx="6" cy="15" r="2"/>
<circle cx="18" cy="15" r="2"/>
<line x1="12" y1="11" x2="6" y2="13"/>
<line x1="12" y1="11" x2="18" y2="13"/>
</svg>
</div>
<div className="landing-nav-wordmark">Resolution<span>Flow</span></div>
</a>
<ul className="landing-nav-links">
<li><a href="#features">Features</a></li>
<li><a href="#how-it-works">How It Works</a></li>
<li><a href="#pricing">Pricing</a></li>
</ul>
<div className="landing-nav-cta">
<Link to="/login" className="landing-btn-ghost">Sign In</Link>
<Link to="/register" className="landing-btn-primary">Get Started Free</Link>
</div>
<button
className={`landing-hamburger ${mobileMenuOpen ? 'open' : ''}`}
onClick={() => setMobileMenuOpen(v => !v)}
aria-label="Toggle menu"
aria-expanded={mobileMenuOpen}
>
<span />
<span />
<span />
</button>
</div>
{mobileMenuOpen && (
<div className="landing-mobile-menu">
<a href="#features" onClick={handleMobileNavClick}>Features</a>
<a href="#how-it-works" onClick={handleMobileNavClick}>How It Works</a>
<a href="#pricing" onClick={handleMobileNavClick}>Pricing</a>
<div className="landing-mobile-menu-divider" />
<Link to="/login" onClick={handleMobileNavClick}>Sign In</Link>
<Link to="/register" className="landing-btn-primary" onClick={handleMobileNavClick} style={{ textAlign: 'center' }}>Get Started Free</Link>
</div>
)}
</nav>
{/* Hero */}
<section className="landing-hero">
<div className="landing-hero-badge">Now in Beta Join early access</div>
<h1>
Resolve tickets faster.<br />
<span className="landing-gradient-text">Notes write themselves.</span>
</h1>
<p className="landing-hero-sub">
ResolutionFlow is your AI troubleshooting copilot. Describe the issue, get expert guidance fixing it, and get clean ticket documentation without writing a single note.
</p>
<div className="landing-hero-actions">
<Link to="/register" className="landing-btn-hero-primary">Start Free</Link>
<a href="#how-it-works" className="landing-btn-hero-secondary">See How It Works</a>
</div>
</section>
{/* Social Proof Bar */}
<div className="landing-social-proof-bar">
<p>Built by MSP engineers, for MSP engineers</p>
<div className="landing-proof-stats">
<div className="landing-proof-stat">
<div className="number">15+</div>
<div className="label">Years MSP Experience</div>
</div>
<div className="landing-proof-stat">
<div className="number">70%</div>
<div className="label">Less Time on Documentation</div>
</div>
<div className="landing-proof-stat">
<div className="number">100%</div>
<div className="label">Auto-Generated Documentation</div>
</div>
</div>
</div>
{/* App Preview */}
<div className="landing-app-preview">
<div className="landing-preview-window">
<div className="landing-preview-titlebar">
<div className="landing-preview-tab">
<div className="landing-tab-icon" />
ResolutionFlow
<span className="landing-tab-close">&times;</span>
</div>
<div className="landing-preview-url-bar">
<div className="landing-preview-url">
<span className="landing-lock-icon">&#128274;</span>
app.resolutionflow.com/pilot
</div>
</div>
<div className="landing-preview-window-controls">
<div className="landing-win-btn">
<svg viewBox="0 0 12 12"><line x1="2" y1="6" x2="10" y2="6"/></svg>
</div>
<div className="landing-win-btn">
<svg viewBox="0 0 12 12"><rect x="2" y="2" width="8" height="8" rx="0.5"/></svg>
</div>
<div className="landing-win-btn close">
<svg viewBox="0 0 12 12"><line x1="2" y1="2" x2="10" y2="10"/><line x1="10" y1="2" x2="2" y2="10"/></svg>
</div>
</div>
</div>
<div className="landing-preview-body">
<div className="landing-preview-sidebar">
<div className="landing-preview-sidebar-item active">
<div className="dot" style={{ background: '#60a5fa' }} />
FlowPilot
</div>
<div className="landing-preview-sidebar-item">
<div className="dot" style={{ background: '#34d399' }} />
Session History
</div>
<div className="landing-preview-sidebar-item">
<div className="dot" style={{ background: '#a78bfa' }} />
Guided Flows
</div>
<div className="landing-preview-sidebar-item">
<div className="dot" style={{ background: '#2dd4bf' }} />
Scripts
</div>
<div className="landing-preview-sidebar-item">
<div className="dot" style={{ background: '#38bdf8' }} />
Analytics
</div>
</div>
<div className="landing-preview-canvas">
<div className="landing-mock-session">
<div className="landing-chat-animated" style={{ '--chat-index': 0 } as React.CSSProperties}>
<div className="landing-mock-chat-line">
<span className="label">You:</span>
<span className="text">User can&apos;t access shared drive after password reset</span>
</div>
</div>
<div className="landing-chat-animated" style={{ '--chat-index': 1 } as React.CSSProperties}>
<div className="landing-typing-indicator">
<span /><span /><span />
</div>
</div>
<div className="landing-chat-animated" style={{ '--chat-index': 2 } as React.CSSProperties}>
<div className="landing-mock-chat-line">
<span className="label" style={{ color: '#60a5fa' }}>FlowPilot:</span>
<span className="text">This is likely a cached credential issue. Let&apos;s check a few things:</span>
</div>
</div>
<div className="landing-chat-animated" style={{ '--chat-index': 3 } as React.CSSProperties}>
<div className="landing-mock-chat-line">
<span className="label" style={{ color: '#60a5fa' }}>FlowPilot:</span>
<span className="text">1. Run <code>klist purge</code> to clear Kerberos tickets</span>
</div>
</div>
<div className="landing-chat-animated" style={{ '--chat-index': 4 } as React.CSSProperties}>
<div className="landing-mock-chat-line">
<span className="label" style={{ color: '#60a5fa' }}>FlowPilot:</span>
<span className="text">2. Open Credential Manager &rarr; remove saved entries for the share</span>
</div>
</div>
<div className="landing-chat-animated" style={{ '--chat-index': 5 } as React.CSSProperties}>
<div className="landing-mock-chat-line doc">
<span className="label">Auto-doc:</span>
<span className="text">3 steps captured &#10003;</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="landing-section-divider" />
{/* Problem Section */}
<section id="problem" className="landing-reveal">
<div className="landing-section-inner">
<div className="landing-section-label">The Problem</div>
<h2 className="landing-section-title">Documentation is broken.<br />Everyone knows it.</h2>
<div className="landing-section-desc">
Engineers don&apos;t want to write it. Managers hate chasing it. Clients never see it. The same issues get solved from scratch every time.
</div>
<div className="landing-problem-grid">
<ProblemCard icon="&#9201;" color="red" title="1525 min lost per ticket" description="Engineers spend more time documenting what they did than actually doing it. After a complex issue, writing notes is the last thing anyone wants to do." />
<ProblemCard icon="&#128203;" color="amber" title="Vague, useless notes" description={`"Fixed Outlook" tells you nothing. Documentation written under pressure tends toward generalities that help nobody the second time around.`} />
<ProblemCard icon="&#128260;" color="slate" title="Knowledge walks out the door" description="When a senior engineer leaves, years of tribal knowledge disappear overnight. New hires spend months building up what was never captured." />
<ProblemCard icon="&#129504;" color="violet" title="Context switching kills speed" description="Jumping between the issue, documentation tools, PSA tickets, and knowledge bases fragments focus and slows resolution." />
</div>
</div>
</section>
<div className="landing-section-divider" />
{/* Brand Equation */}
<div className="landing-equation-section landing-reveal">
<div className="landing-section-label">The Answer</div>
<div className="landing-brand-equation">
<span className="landing-eq-item">Resolution</span>
<span className="landing-eq-operator">+</span>
<span className="landing-eq-item">Documentation</span>
<span className="landing-eq-operator">&minus;</span>
<span className="landing-eq-item">Time</span>
<span className="landing-eq-operator">=</span>
<span className="landing-eq-result">ResolutionFlow</span>
</div>
<p className="landing-equation-desc">
What if documentation was a <em>byproduct</em> of solving the issue not a separate task? What if every ticket your team touched had clean, detailed notes without anyone writing them?
</p>
</div>
<div className="landing-section-divider" />
{/* How It Works */}
<section id="how-it-works" className="landing-reveal">
<div className="landing-section-inner">
<div className="landing-section-label">How It Works</div>
<h2 className="landing-section-title">Three steps. Zero note-writing.</h2>
<div className="landing-section-desc">
Just describe the issue. FlowPilot handles the rest.
</div>
<div className="landing-steps-container">
<div className="landing-step-card">
<h3>Describe the Issue</h3>
<p>Type what&apos;s happening, paste an error message, or drop a screenshot. FlowPilot understands MSP environments AD, Exchange, networking, VPN, you name it.</p>
<div className="landing-step-visual">
<div className="landing-mock-editor">
<div className="landing-mock-node step" style={{ fontSize: '0.7rem', padding: '8px 12px' }}>&#128172; &ldquo;User can&apos;t access shared drive after password reset, getting Access Denied in Event Viewer&rdquo;</div>
</div>
</div>
</div>
<div className="landing-step-card">
<h3>Troubleshoot Together</h3>
<p>FlowPilot acts like a senior engineer on the call with you. It suggests next steps, provides commands to run, and captures every action documentation builds itself as you work.</p>
<div className="landing-step-visual">
<div className="landing-mock-session">
<div className="landing-mock-chat-line">
<span className="label">FlowPilot:</span>
<span className="text">Is the user on VPN?</span>
</div>
<div className="landing-mock-chat-line">
<span className="label" style={{ color: '#848b9b' }}>Engineer:</span>
<span className="text">Yes, Cisco AnyConnect</span>
</div>
<div className="landing-mock-chat-line">
<span className="label">FlowPilot:</span>
<span className="text">Check split tunnel config &rarr;</span>
</div>
<div className="landing-mock-chat-line doc">
<span className="label">Auto-doc:</span>
<span className="text">Step captured &#10003;</span>
</div>
</div>
</div>
</div>
<div className="landing-step-card">
<h3>Resolve &amp; Document</h3>
<p>Hit resolve and get clean, timestamped ticket notes ready to paste into ConnectWise, Atera, or Syncro. Every step you took, every command you ran, documented automatically.</p>
<div className="landing-step-visual">
<div className="landing-mock-ticket">
<div className="landing-mock-ticket-header">ConnectWise Ticket #48291</div>
<div className="landing-mock-ticket-line"><span className="time">10:04</span><span className="check">&#10003;</span><span>Verified VPN connection active</span></div>
<div className="landing-mock-ticket-line"><span className="time">10:06</span><span className="check">&#10003;</span><span>Split tunnel misconfigured fixed</span></div>
<div className="landing-mock-ticket-line"><span className="time">10:08</span><span className="check">&#10003;</span><span>Confirmed Outlook sync restored</span></div>
<div className="landing-mock-ticket-line"><span className="time">10:09</span><span className="check">&#10003;</span><span>Resolution: VPN split tunnel updated</span></div>
</div>
</div>
</div>
</div>
</div>
</section>
<div className="landing-section-divider" />
{/* Features */}
<section id="features" className="landing-reveal">
<div className="landing-section-inner">
<div className="landing-section-label">Features</div>
<h2 className="landing-section-title">Troubleshoot faster.<br />Document everything. Automatically.</h2>
<div className="landing-features-grid">
<FeatureCard
highlight
icon={<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="3"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>}
title="FlowPilot — Your AI Copilot"
description="Like having a senior engineer on every call. Describe the issue, get expert troubleshooting guidance, and documentation writes itself — as a byproduct of solving the problem."
/>
<FeatureCard
icon={<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="9" y1="3" x2="9" y2="21"/></svg>}
title="Guided Troubleshooting Flows"
description="Build step-by-step troubleshooting paths your team can follow. Great for standard procedures, onboarding new engineers, or ensuring consistency."
/>
<FeatureCard
icon={<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>}
title="Zero Empty Ticket Notes"
description="Every troubleshooting session generates timestamped, detailed notes — formatted for your PSA. Your team will never close a ticket with empty notes again."
/>
<FeatureCard
icon={<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>}
title="Team Knowledge That Grows"
description="Every resolved session makes your team smarter. Solutions are saved and surfaced automatically when the next engineer hits a similar issue."
/>
<FeatureCard
icon={<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>}
title="Session History & Analytics"
description="See every troubleshooting session your team has run. Track resolution times, identify common issues, and measure team performance."
/>
<FeatureCard
icon={<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>}
title="PSA Integration"
description="Connect directly to ConnectWise, Atera, and Syncro. Export session docs straight to tickets — no copy-paste needed."
/>
</div>
</div>
</section>
<div className="landing-section-divider" />
{/* Pricing */}
<section id="pricing" className="landing-reveal">
<div className="landing-section-inner">
<div className="landing-section-label">Pricing</div>
<h2 className="landing-section-title">Simple pricing. No surprises.</h2>
<div className="landing-section-desc">Start free. Upgrade when your team is ready.</div>
<div className="landing-pricing-grid">
<PricingCard
name="Free"
target="For individual techs evaluating"
amount="$0"
note="Free forever"
features={['3 decision trees', '20 sessions per month', 'Auto-documentation export', 'Session history (30 days)', 'Community support']}
btnLabel="Get Started"
btnStyle="outline"
/>
<PricingCard
featured
name="Pro"
target="For small MSPs with 15 techs"
amount="$15"
period="/user/mo"
note="Billed monthly or annually"
features={['Unlimited decision trees', 'Unlimited sessions', 'FlowPilot AI copilot', 'Auto-documentation export', 'Full session history', 'Flow templates library', 'Priority support']}
btnLabel="Start Free Trial"
btnStyle="filled"
/>
<PricingCard
name="Team"
target="For growing MSPs with 525 techs"
amount="$25"
period="/user/mo"
note="Billed monthly or annually"
features={['Everything in Pro', 'PSA integration (ConnectWise, Atera, Syncro)', 'Team analytics dashboard', 'Session sharing & collaboration', 'Client context system', 'Role-based permissions', 'Dedicated support']}
btnLabel="Start Free Trial"
btnStyle="outline"
/>
</div>
<p className="landing-pricing-enterprise">
Need Enterprise (25+ techs, SSO, custom branding)?{' '}
<a href="mailto:hello@resolutionflow.com">Contact us</a>
</p>
</div>
</section>
<div className="landing-section-divider" />
{/* Testimonial */}
<div className="landing-testimonial-section landing-reveal">
<div className="landing-testimonial-quote">
We used to spend more time writing ticket notes than solving the actual issue. Now it just&hellip; happens. The documentation writes itself while we work.
</div>
<div className="landing-testimonial-author">
<strong>Beta Tester</strong> MSP Engineer, Southeast US
</div>
</div>
<div className="landing-section-divider" />
{/* CTA */}
<section className="landing-cta-section landing-reveal">
<h2>Ready to never write ticket notes again?</h2>
<p>Join the beta. Troubleshoot your next ticket with FlowPilot and see the documentation write itself.</p>
<form className="landing-cta-email-form" onSubmit={handleBetaSubmit}>
<input
type="email"
className="landing-cta-email-input"
placeholder="you@yourmsp.com"
value={betaEmail}
onChange={e => setBetaEmail(e.target.value)}
required
/>
<button type="submit" className="landing-btn-hero-primary" style={{ whiteSpace: 'nowrap' }} disabled={betaStatus === 'sending'}>
{betaStatus === 'sending' ? 'Joining...' : betaStatus === 'sent' ? 'Joined!' : 'Join Beta'}
</button>
</form>
{betaStatus === 'sent' && (
<p className="landing-cta-success">Thanks! We&apos;ll be in touch with beta access details.</p>
)}
{betaStatus === 'error' && (
<p className="landing-cta-error">Something went wrong. Please try again.</p>
)}
<p className="landing-cta-fine-print">Free to start. No credit card required.</p>
</section>
{/* Footer */}
<footer className="landing-footer">
<div className="landing-footer-inner">
<div className="landing-footer-left">
<div className="landing-nav-logo-icon" style={{ width: 28, height: 28, borderRadius: 8 }}>
<svg viewBox="0 0 24 24" fill="none" stroke="#000" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ width: 16, height: 16 }}>
<circle cx="12" cy="5" r="2"/>
<line x1="12" y1="7" x2="12" y2="11"/>
<circle cx="6" cy="15" r="2"/>
<circle cx="18" cy="15" r="2"/>
<line x1="12" y1="11" x2="6" y2="13"/>
<line x1="12" y1="11" x2="18" y2="13"/>
</svg>
</div>
<span className="landing-footer-copy">&copy; 2026 ResolutionFlow. All rights reserved.</span>
</div>
<ul className="landing-footer-links">
<li><Link to="/privacy">Privacy</Link></li>
<li><Link to="/terms">Terms</Link></li>
<li><a href="mailto:hello@resolutionflow.com">Contact</a></li>
</ul>
</div>
</footer>
</div>
</div>
</>
)
}
/* ---- Sub-components ---- */
function ProblemCard({ icon, color, title, description }: {
icon: string; color: string; title: string; description: string
}) {
return (
<div className="landing-problem-card">
<div className={`landing-problem-icon ${color}`}>{icon}</div>
<h3>{title}</h3>
<p>{description}</p>
</div>
)
}
function FeatureCard({ icon, title, description, highlight }: {
icon: React.ReactNode; title: string; description: string; highlight?: boolean
}) {
return (
<div className={`landing-feature-card ${highlight ? 'highlight' : ''}`}>
<div className="landing-feature-icon">{icon}</div>
<h3>{title}</h3>
<p>{description}</p>
</div>
)
}
function PricingCard({ name, target, amount, period, note, features, btnLabel, btnStyle, featured }: {
name: string; target: string; amount: string; period?: string; note: string
features: string[]; btnLabel: string; btnStyle: 'outline' | 'filled'; featured?: boolean
}) {
return (
<div className={`landing-pricing-card ${featured ? 'featured' : ''}`}>
<div className="landing-pricing-plan-name">{name}</div>
<div className="landing-pricing-target">{target}</div>
<div className="landing-pricing-price">
<span className="amount">{amount}</span>
{period && <span className="period">{period}</span>}
</div>
<div className="landing-pricing-note">{note}</div>
<ul className="landing-pricing-features">
{features.map(f => <li key={f}>{f}</li>)}
</ul>
<Link to="/register" className={`landing-pricing-btn ${btnStyle}`}>{btnLabel}</Link>
</div>
)
}