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>
567 lines
30 KiB
TypeScript
567 lines
30 KiB
TypeScript
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">×</span>
|
||
</div>
|
||
<div className="landing-preview-url-bar">
|
||
<div className="landing-preview-url">
|
||
<span className="landing-lock-icon">🔒</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'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'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 → 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 ✓</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'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="⏱" color="red" title="15–25 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="📋" 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="🔄" 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="🧠" 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">−</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'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' }}>💬 “User can't access shared drive after password reset, getting Access Denied in Event Viewer”</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 →</span>
|
||
</div>
|
||
<div className="landing-mock-chat-line doc">
|
||
<span className="label">Auto-doc:</span>
|
||
<span className="text">Step captured ✓</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="landing-step-card">
|
||
<h3>Resolve & 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">✓</span><span>Verified VPN connection active</span></div>
|
||
<div className="landing-mock-ticket-line"><span className="time">10:06</span><span className="check">✓</span><span>Split tunnel misconfigured — fixed</span></div>
|
||
<div className="landing-mock-ticket-line"><span className="time">10:08</span><span className="check">✓</span><span>Confirmed Outlook sync restored</span></div>
|
||
<div className="landing-mock-ticket-line"><span className="time">10:09</span><span className="check">✓</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 1–5 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 5–25 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… 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'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">© 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>
|
||
)
|
||
}
|