feat: add landing page with beta signup + raise KB node limit to 100

Landing page at /landing with full marketing content: hero, features,
pricing, testimonials, and beta email signup form. Beta signups email
beta@resolutionflow.com via new public endpoint. Unauthenticated users
redirect to landing instead of login. Also raises KB Accelerator node
limit from 50 to 100 to accommodate dense troubleshooting articles.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-03-12 00:23:29 -04:00
parent 92c86cab80
commit 042a12b190
9 changed files with 1756 additions and 4 deletions

View File

@@ -0,0 +1,31 @@
"""Public beta signup endpoint — no auth required."""
import logging
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, EmailStr
from app.core.email import EmailService
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/beta-signup", tags=["beta"])
class BetaSignupRequest(BaseModel):
email: EmailStr
class BetaSignupResponse(BaseModel):
success: bool
message: str
@router.post("", response_model=BetaSignupResponse)
async def beta_signup(data: BetaSignupRequest):
"""Collect beta interest — sends notification to beta@resolutionflow.com."""
sent = await EmailService.send_beta_signup_notification(data.email)
if not sent:
logger.warning("Beta signup recorded (email delivery skipped): %s", data.email)
return BetaSignupResponse(
success=True,
message="Thanks! We'll be in touch with beta access details.",
)

View File

@@ -15,6 +15,7 @@ from app.api.endpoints import admin_survey
from app.api.endpoints import tree_transfer
from app.api.endpoints import ai_suggestions
from app.api.endpoints import kb_accelerator
from app.api.endpoints import beta_signup
api_router = APIRouter()
@@ -54,3 +55,4 @@ api_router.include_router(admin_survey.router)
api_router.include_router(tree_transfer.router)
api_router.include_router(ai_suggestions.router)
api_router.include_router(kb_accelerator.router)
api_router.include_router(beta_signup.router)

View File

@@ -151,9 +151,9 @@ def validate_generated_tree(tree: dict[str, Any]) -> list[str]:
errors.append(
f"Tree has only {node_count} nodes. Minimum 5 required for a useful tree."
)
if node_count > 50:
if node_count > 100:
errors.append(
f"Tree has {node_count} nodes. Maximum 50 allowed."
f"Tree has {node_count} nodes. Maximum 100 allowed."
)
if solution_count < 2:
errors.append(

View File

@@ -418,6 +418,72 @@ class EmailService:
logger.exception("Failed to send survey copy email to %s", to_email)
return False
@staticmethod
async def send_beta_signup_notification(
signup_email: str,
notify_email: str = "beta@resolutionflow.com",
) -> bool:
"""Notify beta@resolutionflow.com about a new beta signup. Fire-and-forget."""
if not settings.email_enabled:
logger.warning("Beta signup email not sent — RESEND_API_KEY not configured")
return False
try:
import resend
import html as html_mod
from datetime import datetime, timezone
resend.api_key = settings.RESEND_API_KEY
date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
safe_email = html_mod.escape(signup_email)
subject = f"[ResolutionFlow Beta] New signup — {safe_email}"
email_html = f"""<!DOCTYPE html>
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"></head>
<body style="margin:0;padding:0;background:#101114;font-family:'Inter',Helvetica,Arial,sans-serif;">
<table width="100%" cellpadding="0" cellspacing="0" style="background:#101114;padding:40px 0;">
<tr><td align="center">
<table width="560" cellpadding="0" cellspacing="0" style="background:#14161a;border:1px solid rgba(255,255,255,0.06);border-radius:16px;">
<tr><td style="padding:40px 40px 24px;text-align:center;">
<h1 style="margin:0;color:#f8fafc;font-size:24px;font-weight:600;">Resolution<span style="color:#06b6d4;">Flow</span></h1>
<p style="margin:8px 0 0;color:#5a6170;font-size:14px;">New Beta Signup</p>
</td></tr>
<tr><td style="padding:0 40px 24px;">
<p style="margin:0;color:#8891a0;font-size:16px;line-height:1.6;">
A new user has requested beta access:
</p>
</td></tr>
<tr><td style="padding:0 40px 24px;text-align:center;">
<div style="background:rgba(0,0,0,0.3);border:1px solid rgba(255,255,255,0.06);border-radius:12px;padding:20px;">
<p style="margin:0 0 4px;color:#5a6170;font-size:12px;text-transform:uppercase;letter-spacing:1px;">Email</p>
<p style="margin:0;color:#22d3ee;font-size:18px;font-weight:600;">{safe_email}</p>
</div>
</td></tr>
<tr><td style="padding:0 40px 32px;">
<p style="margin:0;color:#5a6170;font-size:12px;text-align:center;">
Submitted at {date_str}
</p>
</td></tr>
</table>
</td></tr>
</table>
</body></html>"""
resend.Emails.send({
"from": settings.FROM_EMAIL,
"to": [notify_email],
"reply_to": signup_email,
"subject": subject,
"html": email_html,
})
logger.info("Beta signup notification sent for %s", signup_email)
return True
except Exception:
logger.exception("Failed to send beta signup notification for %s", signup_email)
return False
@staticmethod
async def send_survey_invite_email(
to_email: str,

View File

@@ -139,7 +139,7 @@ Return a JSON object with this structure:
4. The first node is the root of the decision tree.
5. All `next_node_id` and option `next_node_id` references must point to existing node IDs.
6. Detect implicit branching logic (e.g., "If X, do Y; otherwise Z") and create decision nodes.
7. Produce at least 3 nodes. Maximum 50 nodes.
7. Produce at least 3 nodes. Maximum 100 nodes.
8. Use high confidence (0.9+) for directly stated steps, medium (0.7-0.89) for reasonable inferences, low (<0.7) for significant interpretation.
9. Return ONLY valid JSON — no markdown fences, no explanation text."""

View File

@@ -22,7 +22,7 @@ export function ProtectedRoute({ requiredRole, children }: ProtectedRouteProps)
}
if (!isAuthenticated) {
return <Navigate to="/login" state={{ from: location }} replace />
return <Navigate to="/landing" state={{ from: location }} replace />
}
// Enforce must_change_password — redirect unless already on /change-password

View File

@@ -0,0 +1,514 @@
import { useState, useEffect, useCallback } 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 [betaEmail, setBetaEmail] = useState('')
const [betaStatus, setBetaStatus] = useState<'idle' | 'sending' | 'sent' | 'error'>('idle')
// Nav scroll effect
useEffect(() => {
const handleScroll = () => setNavScrolled(window.scrollY > 40)
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}, [])
// 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="AI-guided decision trees that walk your engineers through troubleshooting and automatically document every step."
/>
<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' : ''}`}>
<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>
</div>
</nav>
{/* Hero */}
<section className="landing-hero">
<div className="landing-hero-badge">Now in Beta Join early access</div>
<h1>
Stop writing ticket notes.<br />
<span className="landing-gradient-text">Start generating them.</span>
</h1>
<p className="landing-hero-sub">
AI-guided decision trees that walk your engineers through troubleshooting and automatically document every step, ready for your PSA ticket.
</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">0</div>
<div className="label">Ticket Notes Written by Hand</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/editor
</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: 'var(--cyan-400)' }} />
Flow Editor
</div>
<div className="landing-preview-sidebar-item">
<div className="dot" style={{ background: '#3b82f6' }} />
Session Runner
</div>
<div className="landing-preview-sidebar-item">
<div className="dot" style={{ background: '#22c55e' }} />
Flow Library
</div>
<div className="landing-preview-sidebar-item">
<div className="dot" style={{ background: '#f59e0b' }} />
Session History
</div>
<div className="landing-preview-sidebar-item">
<div className="dot" style={{ background: '#8b5cf6' }} />
Team Analytics
</div>
</div>
<div className="landing-preview-canvas">
<div className="landing-mini-tree">
<div className="landing-tree-node root">Outlook Not Syncing</div>
<div className="landing-tree-connector" />
<div className="landing-tree-branch">
<div className="landing-tree-branch-arm">
<div className="landing-tree-label">Yes</div>
<div className="landing-tree-connector" />
<div className="landing-tree-node decision">Check profile config</div>
</div>
<div className="landing-tree-branch-arm">
<div className="landing-tree-label">No</div>
<div className="landing-tree-connector" />
<div className="landing-tree-node decision">Verify credentials</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div style={{ height: '5rem' }} />
<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>
<div className="landing-section-title">Documentation is broken.<br />Everyone knows it.</div>
<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 your engineers never had to write another ticket note?
</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>
<div className="landing-section-title">Three steps. Zero note-writing.</div>
<div className="landing-section-desc">
Build once, run forever. Every session generates documentation automatically.
</div>
<div className="landing-steps-container">
<div className="landing-step-card">
<h3>Build a Flow</h3>
<p>Use the visual Flow Editor to create branching decision trees for any troubleshooting scenario. Drag, connect, and enrich steps with commands, notes, and AI suggestions.</p>
<div className="landing-step-visual">
<div className="landing-mock-editor">
<div className="landing-mock-node start">&#9654; Start</div>
<div className="landing-mock-connector">&rarr;</div>
<div className="landing-mock-node step">Check DNS</div>
<div className="landing-mock-connector">&rarr;</div>
<div className="landing-mock-node step">Yes / No?</div>
<div className="landing-mock-connector">&rarr;</div>
<div className="landing-mock-node start">&#10003; Resolved</div>
</div>
</div>
</div>
<div className="landing-step-card">
<h3>Run a Session</h3>
<p>An engineer launches the flow on a live ticket. FlowPilot your AI copilot acts as a virtual senior engineer, guiding decisions and capturing every action in real time.</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: 'var(--text-secondary)' }}>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>Export to Ticket</h3>
<p>When the session ends, full documentation is generated formatted for your PSA. Paste it directly into ConnectWise, Atera, or Syncro. Done.</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>
<div className="landing-section-title">Everything your team needs to<br />resolve faster and document better.</div>
<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. FlowPilot suggests next steps, provides context-aware guidance, and automatically captures documentation as a byproduct of the troubleshooting session."
/>
<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="Visual Flow Editor"
description="Build branching decision trees with a drag-and-drop canvas. Add steps, conditions, commands, and notes — no code required."
/>
<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="Auto-Documentation"
description="Every session generates timestamped, detailed notes — formatted for your PSA. Engineers never write another ticket note."
/>
<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 Sharing"
description="Share flows across your team. When one engineer solves a new problem, the whole team benefits from that path — instantly."
/>
<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="Track which flows are used most, identify bottlenecks, and see how your team resolves issues over time."
/>
<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>
<div className="landing-section-title">Simple pricing. No surprises.</div>
<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 stop writing ticket notes?</h2>
<p>Join the beta and see what happens when documentation becomes automatic.</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><a href="#">Privacy</a></li>
<li><a href="#">Terms</a></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>
)
}

View File

@@ -13,6 +13,7 @@ import {
} from '@/pages'
// Public pages
const LandingPage = lazy(() => import('@/pages/LandingPage'))
const SharedSessionPage = lazy(() => import('@/pages/SharedSessionPage'))
const SurveyPage = lazy(() => import('@/pages/SurveyPage'))
const SurveyThankYouPage = lazy(() => import('@/pages/SurveyThankYouPage'))
@@ -78,6 +79,11 @@ function page(Component: React.LazyExoticComponent<React.ComponentType>) {
}
export const router = sentryCreateBrowserRouter([
{
path: '/landing',
element: page(LandingPage),
errorElement: <RouteError />,
},
{
path: '/login',
element: <LoginPage />,

File diff suppressed because it is too large Load Diff