Phase 2 retires the public beta-signup form in favor of the self-serve register flow. The /api/v1/beta-signup POST endpoint stays mounted but now responds with 307 to /register?from=beta so any external links keep working and analytics can tag signup origin via the from query param. Note: there is no beta_signup table in the schema — the original endpoint only fired an email notification, so there is no waitlist to read and no migration to run for the email-sent_at field. The one-off admin script in the spec is therefore a no-op and is intentionally not added here. - Replace POST /beta-signup handler with RedirectResponse(307) - Drop the EmailService.send_beta_signup_notification call (the user is now redirected into the register flow, which has its own email path) - Add tests/test_beta_signup_redirect.py covering the 307 + Location Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
490 lines
26 KiB
TypeScript
490 lines
26 KiB
TypeScript
import { useState, useEffect, useRef } from 'react'
|
|
import { Link } from 'react-router-dom'
|
|
import { PageMeta } from '@/components/common/PageMeta'
|
|
import { useAppConfig } from '@/hooks/useAppConfig'
|
|
import '@/styles/landing.css'
|
|
|
|
const FAQ_ITEMS = [
|
|
{
|
|
q: 'How is this different from just using ChatGPT?',
|
|
a: 'FlowPilot is purpose-built for MSP troubleshooting. It understands your stack (AD, Exchange, networking, VPN), captures every diagnostic step as you work, and generates formatted ticket notes ready for your PSA. ChatGPT doesn\u2019t build documentation and can\u2019t push notes to ConnectWise.',
|
|
},
|
|
{
|
|
q: 'Is my data safe?',
|
|
a: 'Troubleshooting sessions are encrypted and isolated per team. We never use your data to train AI models. You control what gets documented and exported.',
|
|
},
|
|
{
|
|
q: 'What PSA tools do you integrate with?',
|
|
a: 'Launching with ConnectWise PSA \u2014 session documentation exports directly as internal ticket notes. Atera and Syncro integrations are next. During beta, you can copy formatted notes into any PSA.',
|
|
},
|
|
{
|
|
q: 'What counts as a \u201csession\u201d?',
|
|
a: 'One session = one troubleshooting conversation. Describe an issue, work through it with FlowPilot, resolve it. Whether that takes 2 minutes or 2 hours, it\u2019s one session. Free plan: 20 sessions/month. Pro and Team: unlimited.',
|
|
},
|
|
{
|
|
q: 'What if FlowPilot gets it wrong?',
|
|
a: 'FlowPilot is a copilot, not autopilot. Every suggestion is a recommendation \u2014 you decide what to act on. And because every step is documented, you always have a full audit trail of what was tried and why.',
|
|
},
|
|
]
|
|
|
|
export default function LandingPage() {
|
|
const appConfig = useAppConfig()
|
|
const [navScrolled, setNavScrolled] = useState(false)
|
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
|
const [openFaq, setOpenFaq] = useState<number | null>(null)
|
|
const mobileMenuRef = useRef<HTMLDivElement>(null)
|
|
|
|
useEffect(() => {
|
|
const handleScroll = () => setNavScrolled(window.scrollY > 40)
|
|
window.addEventListener('scroll', handleScroll)
|
|
return () => window.removeEventListener('scroll', handleScroll)
|
|
}, [])
|
|
|
|
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])
|
|
|
|
const handleMobileNavClick = () => setMobileMenuOpen(false)
|
|
|
|
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 toggleFaq = (index: number) => {
|
|
setOpenFaq(prev => prev === index ? null : index)
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<PageMeta
|
|
title="ResolutionFlow \u2014 From Issue to Resolution, Documented"
|
|
description="Your AI troubleshooting copilot. Describe the issue, get help fixing it, and get clean ticket notes \u2014 automatically."
|
|
/>
|
|
|
|
<div className="landing-page">
|
|
<a href="#main" className="landing-skip-link">Skip to content</a>
|
|
|
|
{/* 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>
|
|
<li><a href="#faq">FAQ</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>
|
|
<a href="#faq" onClick={handleMobileNavClick}>FAQ</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>
|
|
|
|
<main id="main" className="landing-main">
|
|
{/* Hero — left-aligned, two columns */}
|
|
<section className="landing-hero">
|
|
<div className="landing-hero-inner">
|
|
<div className="landing-hero-content">
|
|
<div className="landing-hero-badge">Now in Beta</div>
|
|
<h1>
|
|
Resolve tickets faster.<br />
|
|
<span className="landing-hero-accent">Notes write themselves.</span>
|
|
</h1>
|
|
<p className="landing-hero-sub">
|
|
Your AI troubleshooting copilot for MSPs. Describe the issue, get expert guidance, 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>
|
|
{appConfig.self_serve_enabled && (
|
|
<Link
|
|
to="/pricing"
|
|
className="landing-btn-hero-secondary"
|
|
data-testid="landing-see-pricing"
|
|
>
|
|
See pricing
|
|
</Link>
|
|
)}
|
|
<a href="#how-it-works" className="landing-btn-hero-secondary">See How It Works</a>
|
|
</div>
|
|
<p className="landing-hero-credibility">
|
|
Built by a 15-year MSP veteran who got tired of empty ticket notes.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Problem — asymmetric: headline left, cards right */}
|
|
<section id="problem" className="landing-section landing-section-alt landing-reveal">
|
|
<div className="landing-section-inner">
|
|
<div className="landing-problem-layout">
|
|
<div className="landing-problem-headline">
|
|
<div className="landing-section-label">The Problem</div>
|
|
<h2>Documentation is broken.<br />Everyone knows it.</h2>
|
|
<p>Engineers don't want to write it. Managers hate chasing it. Clients never see it. The same issues get solved from scratch — every time.</p>
|
|
</div>
|
|
<div className="landing-problem-grid">
|
|
<ProblemCard icon="⏱" color="red" title="15–25 min lost per ticket" description="More time documenting than resolving. After a complex issue, writing notes is the last thing anyone does." />
|
|
<ProblemCard icon="📋" color="amber" title="Vague, useless notes" description={`"Fixed Outlook" tells no one anything. Notes under pressure are always too vague to help next time.`} />
|
|
<ProblemCard icon="🔄" color="slate" title="Knowledge walks out the door" description="When a senior engineer leaves, years of tribal knowledge vanish overnight." />
|
|
<ProblemCard icon="🧠" color="violet" title="Context switching kills speed" description="Jumping between the issue, docs, PSA tickets, and knowledge bases fragments focus." />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Equation */}
|
|
<div className="landing-equation-section landing-reveal">
|
|
<div className="landing-equation-inner">
|
|
<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?
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* How It Works — zigzag */}
|
|
<section id="how-it-works" className="landing-section 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>
|
|
<div className="landing-zigzag">
|
|
<div className="landing-zigzag-step">
|
|
<div className="landing-zigzag-text">
|
|
<div className="landing-zigzag-number">01</div>
|
|
<h3>Describe the Issue</h3>
|
|
<p>Type what's happening, paste an error, or drop a screenshot. FlowPilot understands MSP environments — AD, Exchange, networking, VPN, you name it.</p>
|
|
</div>
|
|
<div className="landing-zigzag-visual">
|
|
<div className="landing-mock-input">
|
|
<span className="landing-mock-input-icon">💬</span>
|
|
<span>User can't access shared drive after password reset, getting Access Denied in Event Viewer</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="landing-zigzag-step reverse">
|
|
<div className="landing-zigzag-text">
|
|
<div className="landing-zigzag-number">02</div>
|
|
<h3>Troubleshoot Together</h3>
|
|
<p>FlowPilot acts like a senior engineer on the call. It suggests next steps, provides commands, and captures every action — documentation builds itself as you work.</p>
|
|
</div>
|
|
<div className="landing-zigzag-visual">
|
|
<div className="landing-mock-session compact">
|
|
<div className="landing-mock-chat-line ai">
|
|
<span className="label">FlowPilot</span>
|
|
<span className="text">Is the user on VPN?</span>
|
|
</div>
|
|
<div className="landing-mock-chat-line user">
|
|
<span className="label">You</span>
|
|
<span className="text">Yes, Cisco AnyConnect</span>
|
|
</div>
|
|
<div className="landing-mock-chat-line ai">
|
|
<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-zigzag-step">
|
|
<div className="landing-zigzag-text">
|
|
<div className="landing-zigzag-number">03</div>
|
|
<h3>Resolve & Document</h3>
|
|
<p>Hit resolve and get clean, timestamped ticket notes — ready to paste into ConnectWise, Atera, or Syncro. Every step documented automatically.</p>
|
|
</div>
|
|
<div className="landing-zigzag-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>
|
|
</section>
|
|
|
|
{/* Features */}
|
|
<section id="features" className="landing-section landing-section-alt landing-reveal">
|
|
<div className="landing-section-inner">
|
|
<div className="landing-section-label">Features</div>
|
|
<h2 className="landing-section-title">Everything you need to troubleshoot faster.</h2>
|
|
|
|
<div className="landing-feature-highlight">
|
|
<div className="landing-feature-highlight-icon">
|
|
<svg width="28" height="28" 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>
|
|
</div>
|
|
<div className="landing-feature-highlight-content">
|
|
<h3>FlowPilot — Your AI Copilot</h3>
|
|
<p>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.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="landing-features-grid">
|
|
<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 Flows"
|
|
description="Build step-by-step troubleshooting paths your team can follow. Great for onboarding and 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 Tickets"
|
|
description="Every session generates timestamped notes, formatted for your PSA. No more empty ticket closures."
|
|
/>
|
|
<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"
|
|
description="Solutions are saved and surfaced 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 Analytics"
|
|
description="Track resolution times, identify recurring 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 to ConnectWise, Atera, and Syncro. Push session docs straight to tickets."
|
|
/>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Pricing */}
|
|
<section id="pricing" className="landing-section landing-reveal">
|
|
<div className="landing-section-inner">
|
|
<div className="landing-section-label">Pricing</div>
|
|
<h2 className="landing-section-title">Simple pricing. No surprises.</h2>
|
|
<p className="landing-section-desc">Start free. Upgrade when your team is ready.</p>
|
|
<div className="landing-pricing-grid">
|
|
<PricingCard
|
|
name="Free"
|
|
target="Individual techs evaluating"
|
|
amount="$0"
|
|
note="Free forever"
|
|
features={['3 guided flows', '20 sessions per month', 'Auto-documentation export', '30-day session history']}
|
|
btnLabel="Get Started"
|
|
btnStyle="outline"
|
|
plan="free"
|
|
/>
|
|
<PricingCard
|
|
featured
|
|
name="Pro"
|
|
target="Small MSPs · 1–5 techs"
|
|
amount="$15"
|
|
period="/user/mo"
|
|
note="Billed monthly or annually"
|
|
features={['Unlimited flows & sessions', 'FlowPilot AI copilot', 'Full session history', 'Flow templates library', 'Priority support']}
|
|
btnLabel="Start Free Trial"
|
|
btnStyle="filled"
|
|
plan="pro"
|
|
/>
|
|
<PricingCard
|
|
name="Team"
|
|
target="Growing MSPs · 5–25 techs"
|
|
amount="$25"
|
|
period="/user/mo"
|
|
note="Billed monthly or annually"
|
|
features={['Everything in Pro', 'PSA integration', 'Team analytics dashboard', 'Session sharing', 'Role-based permissions', 'Dedicated support']}
|
|
btnLabel="Start Free Trial"
|
|
btnStyle="outline"
|
|
plan="team"
|
|
/>
|
|
</div>
|
|
<p className="landing-pricing-session-note">One session = one troubleshooting conversation, regardless of length.</p>
|
|
<p className="landing-pricing-enterprise">
|
|
Enterprise (25+ techs, SSO, custom branding)?{' '}
|
|
<a href="mailto:hello@resolutionflow.com">Let's talk</a>
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
{/* FAQ */}
|
|
<section id="faq" className="landing-section landing-section-alt landing-reveal">
|
|
<div className="landing-section-inner">
|
|
<div className="landing-section-label">FAQ</div>
|
|
<h2 className="landing-section-title">Common questions</h2>
|
|
<div className="landing-faq-list">
|
|
{FAQ_ITEMS.map((item, i) => (
|
|
<div key={i} className={`landing-faq-item ${openFaq === i ? 'open' : ''}`}>
|
|
<button
|
|
className="landing-faq-trigger"
|
|
onClick={() => toggleFaq(i)}
|
|
aria-expanded={openFaq === i}
|
|
>
|
|
<span>{item.q}</span>
|
|
<span className="landing-faq-icon" aria-hidden="true">{openFaq === i ? '\u2212' : '+'}</span>
|
|
</button>
|
|
<div className="landing-faq-answer" role="region">
|
|
<p>{item.a}</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Founder — replaces anonymous testimonial */}
|
|
<div className="landing-founder-section landing-reveal">
|
|
<div className="landing-founder-inner">
|
|
<div className="landing-section-label">Why We Built This</div>
|
|
<blockquote>
|
|
After 15 years in the MSP trenches, I got tired of the same cycle: solve the issue in 10 minutes, spend 20 minutes writing notes about it. Or worse — close the ticket with “Fixed issue” because there's no time. ResolutionFlow is the tool I wanted on every call.
|
|
</blockquote>
|
|
<div className="landing-founder-name">— Michael, Founder</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* CTA */}
|
|
<section className="landing-cta-section landing-reveal">
|
|
<div className="landing-cta-inner">
|
|
<h2>Ready to stop writing ticket notes?</h2>
|
|
<p>Get early access. Troubleshoot your next ticket with FlowPilot.</p>
|
|
<div className="landing-cta-actions">
|
|
<Link to="/register?from=beta" className="landing-btn-hero-primary">Get started</Link>
|
|
</div>
|
|
<p className="landing-cta-fine-print">Free to start. No credit card required.</p>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Footer */}
|
|
<footer className="landing-footer">
|
|
<div className="landing-footer-inner">
|
|
<div className="landing-footer-left">
|
|
<div className="landing-nav-logo-icon" style={{ width: 24, height: 24, borderRadius: 6 }}>
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="#000" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ width: 14, height: 14 }}>
|
|
<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</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>
|
|
</main>
|
|
</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 }: {
|
|
icon: React.ReactNode; title: string; description: string
|
|
}) {
|
|
return (
|
|
<div className="landing-feature-card">
|
|
<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, plan }: {
|
|
name: string; target: string; amount: string; period?: string; note: string
|
|
features: string[]; btnLabel: string; btnStyle: 'outline' | 'filled'; featured?: boolean; plan: string
|
|
}) {
|
|
return (
|
|
<div className={`landing-pricing-card ${featured ? 'featured' : ''}`}>
|
|
{featured && <div className="landing-pricing-badge">Most Popular</div>}
|
|
<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?plan=${plan}`} className={`landing-pricing-btn ${btnStyle}`}>{btnLabel}</Link>
|
|
</div>
|
|
)
|
|
}
|